gateway: a call Codex makes for itself says so on the Routing page, and goes to a pool account with what Codex says about it (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna) — magpie changes no model: Codex sends its own calls on Luna (the guardian review of an approval, x-openai-subagent: guardian; a thread's title; memories) and, on Luna Reserve once the plan's allowance is used up, every turn with x-openai-codex-luna-reserve. Relayed to Codex's own sign-in these headers went on as they came, but a turn the pool of Codex's accounts served was signed afresh and went without them. Now x-openai-*, x-codex-* (turn metadata, installation, window, parent thread), session_id, conversation_id, x-client-request-id and version go on with it too; never a sign-in (Authorization, chatgpt-account-id, anything with authorization in its name) nor Codex's device attestation, the pool account's own going in its place, nor what holds for Codex's own account alone (x-openai-codex-luna-reserve, as another account's allowance may be left, and x-codex-turn-state, the backend's routing of the turn for that account). The trace, the Recent calls and the usage log keep the call's kind (kind, from x-openai-subagent; memgen; luna_reserve), and the Requests list, a replay and the request's story show it as a grey tag by the model — Approval check / 审批判定, Title / 标题, Memory / 记忆, Subagent / 子代理, Review / 审查, Compaction / 压缩, Luna Reserve / Luna 储备, an unknown one as Codex names it — the story saying Codex made the call itself and picks its model. The Requests list's model column fits its content (up to 36%) so model and tag keep their room, the tag giving way first. Tested with a fake ChatGPT backend on the pool path (acct-1 used up, acct-2 answering: both get the headers and neither the client's sign-in; fails without the change), callKind/codexHeader cases, and a new internal/gui/tests/routing-kind.test.cjs in Chromium and WebKit, en and zh (tags, the model shown whole, the story, a click leaving the row where it is); the other GUI tests, go build -tags nogui, GOOS=windows go build and go test -tags nogui pass

This commit is contained in:
yetone committed 2026-09-29 17:56:43 +08:00
1 parent bdb1dfdd1b
commit f9c99cf370
10 files changed
+323 -12

No files matched your search

+101
View File
@@ -148,3 +148,104 @@ func TestResetsIn(t *testing.T) {
}
}
}
// A turn served by the pool of Codex's accounts goes on with what Codex
// says about it — a guardian review, the turn's metadata, Luna Reserve —
// as it would to Codex's own sign-in, signed by the pool's account and
// never by the client's; the Routing view and the usage log say it was a
// guardian review (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示
// 的全是 gpt-6-luna).
func TestCodexPoolForwardsCodexHeaders(t *testing.T) {
codexSignedIn(t, "spare@example.com")
var heads []http.Header
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.ReadAll(r.Body)
heads = append(heads, r.Header.Clone())
if r.Header.Get("chatgpt-account-id") == "acct-1" {
w.WriteHeader(429)
io.WriteString(w, `{"error":{"type":"usage_limit_reached","message":"The usage limit has been reached","resets_in_seconds":7200}}`)
return
}
io.WriteString(w, sse(
`data: {"type":"response.created","response":{"id":"r1","model":"gpt-5.5"}}`,
`data: {"type":"response.output_text.delta","delta":"pong"}`,
`data: {"type":"response.completed","response":{"id":"r1","usage":{"input_tokens":7,"output_tokens":1}}}`))
}))
t.Cleanup(up.Close)
was := provider.CodexBase
provider.CodexBase = up.URL + "/backend-api/codex"
t.Cleanup(func() { provider.CodexBase = was })
s := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", CodexPath+"/responses", strings.NewReader(`{"model":"gpt-5.5","stream":true,"input":"ping"}`))
req.Header.Set("Authorization", "Bearer chatgpt-token")
req.Header.Set("chatgpt-account-id", "acct-1")
req.Header.Set("User-Agent", "codex_cli_rs/0.155.1")
req.Header.Set("x-openai-subagent", "guardian")
req.Header.Set("x-codex-turn-metadata", `{"turn_id":"t1"}`)
req.Header.Set("x-openai-codex-luna-reserve", "true")
req.Header.Set("x-codex-installation-id", "inst-1")
req.Header.Set("x-codex-turn-state", "acct-1-state")
req.Header.Set("x-oai-attestation", "device-proof")
req.Header.Set("x-openai-actor-authorization", "Bearer someone")
s.Handler().ServeHTTP(rec, req)
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "pong") {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
if len(heads) != 2 {
t.Fatalf("%d requests upstream", len(heads))
}
for i, h := range heads {
for k, want := range map[string]string{
"x-openai-subagent": "guardian",
"x-codex-turn-metadata": `{"turn_id":"t1"}`,
"x-codex-installation-id": "inst-1",
"chatgpt-account-id": "acct-" + string(rune('1'+i)),
} {
if got := h.Get(k); got != want {
t.Errorf("request %d: %s %q, want %q", i, k, got, want)
}
}
if h.Get("x-openai-codex-luna-reserve") != "" || h.Get("x-codex-turn-state") != "" {
t.Errorf("request %d carries what holds for Codex's own account: %v", i, h)
}
if h.Get("Authorization") == "Bearer chatgpt-token" || h.Get("x-oai-attestation") != "" || h.Get("x-openai-actor-authorization") != "" {
t.Errorf("request %d carries the client's sign-in: %v", i, h)
}
}
if r := s.trace.routes[len(s.trace.routes)-1]; r.Kind != "guardian" {
t.Errorf("route kind %q", r.Kind)
}
b, _ := os.ReadFile(filepath.Join(filepath.Dir(provider.Path()), "usage.jsonl"))
if !strings.Contains(string(b), `"kind":"guardian"`) {
t.Errorf("usage log: %s", b)
}
}
func TestCallKind(t *testing.T) {
for want, h := range map[string]map[string]string{
"": {},
"guardian": {"x-openai-subagent": " guardian ", "x-openai-codex-luna-reserve": "true"},
"thread_title": {"x-openai-subagent": "thread_title"},
"memgen": {"x-openai-memgen-request": "1"},
"luna_reserve": {"x-openai-codex-luna-reserve": "true"},
} {
hh := http.Header{}
for k, v := range h {
hh.Set(k, v)
}
if got := callKind(hh); got != want {
t.Errorf("%v: %q, want %q", h, got, want)
}
}
for k, want := range map[string]bool{
"x-openai-subagent": true, "X-Codex-Turn-Metadata": true, "X-Codex-Turn-State": false, "x-openai-codex-luna-reserve": false, "session_id": true, "Session_id": true,
"Authorization": false, "chatgpt-account-id": false, "x-oai-attestation": false,
"x-openai-actor-authorization": false, "User-Agent": false, "Cookie": false,
} {
if codexHeader(k) != want {
t.Errorf("codexHeader(%q) = %v", k, !want)
}
}
}
+45 -3
View File
@@ -224,7 +224,7 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
}
model := modelOf(body)
seat := Weighed{ID: "codex", Provider: "openai", Name: "OpenAI", Icon: "openai", Who: who, Kind: "account", Agent: "codex", Model: model}
tr = s.trace.begin(Route{Time: start, Agent: agentOf(r), Model: model, Provider: "openai",
tr = s.trace.begin(Route{Time: start, Agent: agentOf(r), Kind: callKind(r.Header), Model: model, Provider: "openai",
Order: []Weighed{seat}, Tries: []Try{{ID: seat.ID, Model: model, Start: start}}})
end = func(status int, msg string, tokens, out int) {
ms := time.Since(start).Milliseconds()
@@ -321,7 +321,7 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
return
}
call := Call{Time: start, From: provider.Responses, To: provider.Responses, Model: modelOf(body),
Provider: "openai", Agent: agentOf(r), Status: res.StatusCode,
Provider: "openai", Agent: agentOf(r), Kind: callKind(r.Header), Status: res.StatusCode,
Millis: time.Since(start).Milliseconds()}
call.TTFT, call.FirstText = first.ms()
var uu Usage
@@ -334,7 +334,7 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
s.record(call)
usage.Append(usage.Record{Time: start, Agent: call.Agent, Provider: call.Provider, Host: provider.HostOf(base), Model: call.Model,
Input: uu.Input, Output: uu.Output, CacheRead: uu.CacheRead, CacheWrite: uu.CacheWrite,
Reasoning: uu.Reasoning, Millis: call.Millis, TTFT: call.TTFT, FirstText: call.FirstText, Status: call.Status, Session: sessionOf(r.Header)})
Reasoning: uu.Reasoning, Millis: call.Millis, TTFT: call.TTFT, FirstText: call.FirstText, Status: call.Status, Session: sessionOf(r.Header), Kind: call.Kind})
}
// unreadableItem is the item OpenAI's refusal names: sealed content it
@@ -418,6 +418,48 @@ func hopHeader(k string) bool {
return false
}
// codexHeader is one of the headers Codex tells the ChatGPT backend about
// a request by, which go on with it when a pool account signs it as they
// do when Codex's own sign-in does (codexUpstream): x-openai-subagent (a
// guardian review, a thread's title, memories, a compaction…), the turn's
// x-codex-turn-metadata, its installation, window and parent thread, and
// the session. Never a sign-in: the account's own goes in its place, and
// Codex's device attestation is its own sign-in's. Nor what holds for
// Codex's own account alone: x-openai-codex-luna-reserve, which says its
// plan's allowance is used up (another account's may not be), and
// x-codex-turn-state, the backend's routing of the turn for that account.
func codexHeader(k string) bool {
k = strings.ToLower(k)
if strings.Contains(k, "authorization") || k == "x-oai-attestation" ||
k == "x-openai-codex-luna-reserve" || k == "x-codex-turn-state" {
return false
}
switch k {
case "session_id", "conversation_id", "x-client-request-id", "version":
return true
}
return strings.HasPrefix(k, "x-openai-") || strings.HasPrefix(k, "x-codex-")
}
// callKind is what an agent made a call for when it isn't a turn of the
// conversation, as Codex names it in x-openai-subagent: "guardian" (auto
// review of an approval), "review", "compact", "memory_consolidation",
// "thread_title", "collab_spawn"… A turn Codex sends on Luna Reserve, once
// the plan's own allowance is used up, is "luna_reserve".
func callKind(h http.Header) string {
v := strings.TrimSpace(h.Get("x-openai-subagent"))
if v == "" && h.Get("x-openai-memgen-request") != "" {
v = "memgen"
}
if v == "" && h.Get("x-openai-codex-luna-reserve") != "" {
v = "luna_reserve"
}
if len(v) > 40 {
v = v[:40]
}
return v
}
func copyHeaders(dst, src http.Header) {
for k, vs := range src {
if !hopHeader(k) && http.CanonicalHeaderKey(k) != "Host" {
+18 -5
View File
@@ -153,8 +153,11 @@ func Serving() (running, window bool) {
// Call is one request the gateway handled, for the status views.
type Call struct {
Time time.Time `json:"time"`
Agent string `json:"agent"` // who called, from the client's User-Agent
Time time.Time `json:"time"`
Agent string `json:"agent"` // who called, from the client's User-Agent
// Kind: what the agent made the call for, when it isn't its turn —
// a Codex subagent's (callKind) — "" for a turn
Kind string `json:"kind,omitempty"`
Model string `json:"model"`
Provider string `json:"provider"`
From provider.Protocol `json:"from"`
@@ -643,7 +646,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
requestBody, requestTruncated := captureRequestBody(body)
capture := &captureResponseWriter{ResponseWriter: w}
w = capture
call := Call{Time: start, From: from, Model: unprefixed(modelOf(body)), Agent: agentOf(r),
call := Call{Time: start, From: from, Model: unprefixed(modelOf(body)), Agent: agentOf(r), Kind: callKind(r.Header),
RequestBody: requestBody, RequestTruncated: requestTruncated}
usage.Saw(call.Agent)
finishCapture := func() {
@@ -873,7 +876,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
if len(cands) == 1 {
shown = nil // nobody else to stay away from
}
tr := s.trace.begin(Route{Time: start, Agent: call.Agent, Model: call.Model, Effort: requestEffort(from, body), Provider: p.ID, Group: group, Rule: hit, Nested: nested, Affinity: shown, Order: pl.order, Left: pl.left})
tr := s.trace.begin(Route{Time: start, Agent: call.Agent, Kind: call.Kind, Model: call.Model, Effort: requestEffort(from, body), Provider: p.ID, Group: group, Rule: hit, Nested: nested, Affinity: shown, Order: pl.order, Left: pl.left})
var skipped []string
sent := "" // the reasoning the last try's model was asked for
where := "" // the last try's provider.Where, for the usage
@@ -1047,7 +1050,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
usage.Append(usage.Record{Time: start, Agent: call.Agent, Provider: call.Provider, Host: where, Model: model,
Input: call.Usage.Input, Output: call.Usage.Output, CacheRead: call.Usage.CacheRead,
CacheWrite: call.Usage.CacheWrite, Reasoning: call.Usage.Reasoning, Effort: sent, Millis: call.Millis, Status: call.Status,
TTFT: call.TTFT, FirstText: call.FirstText, Session: sessionOf(r.Header)})
TTFT: call.TTFT, FirstText: call.FirstText, Session: sessionOf(r.Header), Kind: call.Kind})
}
}
@@ -1184,6 +1187,16 @@ func (s *Server) forwardOnce(ctx context.Context, p provider.Provider, to provid
if p.IsOpenCode() {
req.Header.Set("x-opencode-session", conversationID(in, body))
}
if p.Account != nil && p.Account.Agent == "codex" {
// what Codex says about the request goes on as codexUpstream
// relays it — a subagent's kind, the turn's metadata, a turn on
// Luna Reserve — the account signing it being the pool's
for k, vs := range in {
if codexHeader(k) {
req.Header[k] = slices.Clone(vs)
}
}
}
if err := p.Sign(ctx, req, to, body); err != nil {
return nil, err
}
+1
View File
@@ -26,6 +26,7 @@ type Route struct {
ID int64 `json:"id"`
Time time.Time `json:"time"`
Agent string `json:"agent"`
Kind string `json:"kind,omitempty"` // what the call is for, as Call's
Model string `json:"model"` // as the agent asked
Effort string `json:"effort,omitempty"` // the reasoning the agent asked for; "" for none
Provider string `json:"provider"` // the provider the model resolved to
+9
View File
@@ -1066,6 +1066,15 @@ const I18N = {
"the others go in their order.": "其余按顺序排。",
"In order: with {rested} resting after a failure, {who} is the first that can answer.": "按顺序:{rested} 失败后正在休息,{who} 是第一个能答的。",
"{agent} asked for {model}": "{agent} 请求 {model}",
"Approval check": "审批判定",
"Review": "审查",
"Compaction": "压缩",
"Memory": "记忆",
"Title": "标题",
"Subagent": "子代理",
"Luna Reserve": "Luna 储备",
"{agent} sent this turn on Luna Reserve, which it turns to once the plan's own allowance is used up; it picks the model itself.": "{agent} 用 Luna 储备发出了这一轮:套餐自己的额度用完后它会改用 Luna 储备,模型由它自己决定。",
"{agent} made this call itself ({kind}), not as a turn of the conversation, and picks its model itself.": "这是 {agent} 自己发起的调用({kind}),不是对话中的一轮,模型由它自己决定。",
"Nothing was tried: {error}": "没有尝试任何账号:{error}",
"Requests": "请求",
"{agent} asked for {model}: {name} serves it, and the vendor is asked for {sent}": "{agent} 请求 {model}:由 {name} 提供,向厂商请求的模型是 {sent}",
+7 -1
View File
@@ -150,7 +150,7 @@
.rt-cols { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: 16px; }
@container (min-width: 860px) { .rt-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } }
.rt-col { min-width: 0; }
.rt-reqs { padding: 4px; max-height: 420px; overflow-y: auto; display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.4fr) auto; }
.rt-reqs { padding: 4px; max-height: 420px; overflow-y: auto; display: grid; grid-template-columns: auto fit-content(36%) minmax(0, 1fr) auto; }
.rt-req { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: center; column-gap: 12px; padding: 6px 8px; font: inherit; font-size: 12px; text-align: left; color: var(--fg-2); background: none; border: 1px solid transparent; border-radius: 8px; cursor: pointer; }
.rt-req:hover { background: var(--card-2); }
.rt-req[aria-pressed="true"] { background: var(--card-2); border-color: var(--line); color: var(--fg); }
@@ -168,6 +168,12 @@
.rt-req .to .ef { flex: none; overflow: visible; margin-left: -2px; color: var(--muted); }
.rt-req .ef::before { content: "· "; }
.rt-req .to .ef.picked { color: var(--fg-2); }
/* what a call was for (a Codex guardian review, a title…): a quiet grey tag by the model */
.rt-req .asked .kind, .rp-req .kind, .rt-steps li .kind { flex: none; font-size: 10.5px; line-height: 16px; padding: 0 5px; border-radius: 5px; color: var(--muted); background: var(--card-2); border: 1px solid var(--line); white-space: nowrap; }
.rt-steps li .kind { display: inline-block; vertical-align: 1px; }
/* in a narrow row the model keeps its room first: the tag gives way, to an ellipsis */
.rt-req .asked .kind { flex: 0 100 auto; min-width: 3.2em; max-width: 9em; overflow: hidden; text-overflow: ellipsis; }
.rt-req .asked.kinded .m { flex: none; max-width: calc(100% - 3.2em - 40px); }
.rt-req .meta { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
@container (max-width: 520px) { .rt-reqs { grid-template-columns: auto minmax(0, 1fr); } .rt-req { row-gap: 2px; } .rt-req .to { grid-column: 2; } .rt-req .meta { grid-column: 2; text-align: left; } }
.rt-acts { padding: 4px 0; }
+32 -2
View File
@@ -903,6 +903,7 @@
: main && main.model !== r.model
? t("{agent} asked for {model}: {name} serves it, and the vendor is asked for {sent}", { agent: agentName(r.agent), model: r.model, name: main.name, sent: main.model })
: t("{agent} asked for {model}", { agent: agentName(r.agent), model: r.model }) + " → " + (main?.name || r.provider), ""]);
if (r.kind) items.push([kindWhy(r), "aside kind"]);
items.push([affWhy(r, true) || ruleWhy(r, true) || firstWhy(r), "why"]);
for (const s of nestedWhy(r)) items.push([s, "why"]);
for (const a of asides(r)) items.push([a, "aside"]);
@@ -912,7 +913,11 @@
if (tr.done && tr.status >= 400 && tr.error) items.push([t("It said: {error}", { error: tr.error.length > 600 ? tr.error.slice(0, 600) + "…" : tr.error }), "aside said"]);
});
if (r.done && !r.tries.length) items.push([t("Nothing was tried: {error}", { error: r.error || r.status }), "bad"]);
steps.replaceChildren(...items.map(([s, c]) => el("li", c, s)));
steps.replaceChildren(...items.map(([s, c]) => {
const li = el("li", c, s);
if (c === "aside kind") li.prepend(kindTag(r), " ");
return li;
}));
}
// pick sets the stage to a past request, or back to live with the newest.
@@ -928,6 +933,30 @@
say(affWhy(r, true) || ruleWhy(r, true) || firstWhy(r));
}
// what a call was for when it isn't a turn of the conversation, as
// Codex names it (x-openai-subagent): its own guardian review of an
// approval, a thread's title, memories… — each on the model Codex picks
// for it, so a list of Luna calls under a Sol composer reads as it is
const KIND = {
guardian: "Approval check", auto_review: "Approval check", guardian_review: "Approval check",
review: "Review", compact: "Compaction",
memory_consolidation: "Memory", memgen: "Memory", memory: "Memory",
thread_title: "Title", title: "Title",
collab_spawn: "Subagent", thread_spawn: "Subagent", agent_job: "Subagent",
luna_reserve: "Luna Reserve",
};
const kindName = (k) => KIND[k] ? t(KIND[k]) : k;
function kindTag(r) {
const k = el("span", "kind", kindName(r.kind));
k.title = kindWhy(r);
return k;
}
function kindWhy(r) {
const agent = agentName(r.agent);
if (r.kind === "luna_reserve") return t("{agent} sent this turn on Luna Reserve, which it turns to once the plan's own allowance is used up; it picks the model itself.", { agent });
return t("{agent} made this call itself ({kind}), not as a turn of the conversation, and picks its model itself.", { agent, kind: kindName(r.kind) });
}
// who answered a request, or what its agent got
function outcome(r) {
if (!r.done) {
@@ -1021,6 +1050,7 @@
const sw = el("i", "ag");
sw.style.setProperty("--agent", hueOf(r.agent));
asked.append(sw, icon(ag?.icon || "generic"), el("span", "m", r.model));
if (r.kind) { asked.classList.add("kinded"); asked.append(kindTag(r)); }
// the reasoning the model was sent at — the turn's pick, or the
// agent's fitted to the model's levels; what the agent asked for is
// in its title and the request's story
@@ -1403,7 +1433,7 @@
sw.style.setProperty("--agent", hueOf(r.agent));
const [said] = outcome(r);
row.append(el("span", "rp-n", t("request {i} of {n}", { i: g.n, n: p.plan.length })), sw,
el("b", "", agentName(r.agent)), el("code", "mdl", r.model), el("span", "rp-to", "→ " + said),
el("b", "", agentName(r.agent)), el("code", "mdl", r.model), ...(r.kind ? [kindTag(r)] : []), el("span", "rp-to", "→ " + said),
el("span", "rp-at", t("sent {time}", { time: new Date(r.time).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }) })));
row.onclick = () => { cur = r; renderLog(); };
return row;
+6 -1
View File
@@ -23,10 +23,15 @@ was clicked where it is on every frame.
scrolled: its fields hide, the head keeps the base URL and a copy button, the
head stays where it was, and the fold is remembered across a reload.
`routing-kind.test.cjs` lists calls Codex makes for itself (a guardian review,
a title, memories, a turn on Luna Reserve, a kind it does not know yet): each
has a grey tag by its model in the Requests list, in English and Chinese, the
model keeping its room first, and the request's story says what it was.
With Node.js and Playwright available:
```sh
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/gateway-fold.test.cjs
node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs
```
If Playwright is installed outside the repository, set `NODE_PATH` to the
+101
View File
@@ -0,0 +1,101 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// A call Codex makes for itself — its guardian review of an approval, a
// thread's title, memories — or a turn on Luna Reserve, carries a grey tag
// by the model in the Routing page's Requests list and in the request's
// story, so a list of Luna calls under a Sol composer reads as it is
// (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna).
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const now = new Date();
const day = [now.getFullYear(), now.getMonth() + 1, now.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
const at = (i) => new Date(now.getTime() - (i + 1) * 60e3).toISOString();
const seat = { id: "codex", provider: "openai", name: "OpenAI", who: "Codex's own sign-in", kind: "account", agent: "codex", model: "gpt-6-luna" };
const kinds = ["guardian", "thread_title", "memory_consolidation", "luna_reserve", "something_new", ""];
const routes = kinds.map((kind, i) => ({
id: 100 - i, seq: 100 - i, time: at(i), agent: "codex", model: "gpt-6-luna", provider: "openai", ...(kind ? { kind } : {}),
order: [seat], tries: [{ id: seat.id, model: "gpt-6-luna", start: at(i), done: true, status: 200, ms: 900 }],
done: true, status: 200, ms: 900, tokens: 1200,
}));
function serve(lang) {
const state = { agents: [{ id: "codex", name: "Codex", path: "/test/config.toml", fields: [] }], profiles: [], settings: { lang, theme: "light" } };
return async (route) => {
const url = new URL(route.request().url());
const json = (data) => route.fulfill({ json: data });
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json(state);
if (url.pathname === "/api/gateway/trace") {
if (url.searchParams.get("wait")) await new Promise((r) => setTimeout(r, 20e3));
return json({ mine: true, now: now.toISOString(), seq: 1, totals: { requests: 0, rerouted: 0, errors: 0 }, routes: [] });
}
if (url.pathname === "/api/gateway/history") {
const d = url.searchParams.get("day");
return json({ cut: false, days: [{ day, requests: routes.length }], routes: d ? routes : [] });
}
if (url.pathname === "/api/groups") return json({ groups: [] });
if (url.pathname === "/api/providers") return json({ providers: [], gateway: { running: true, window: true } });
if (url.pathname.startsWith("/api/")) return json({});
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
await route.fulfill({ body: await fs.readFile(file), contentType });
};
}
const want = {
en: ["Approval check", "Title", "Memory", "Luna Reserve", "something_new"],
zh: ["审批判定", "标题", "记忆", "Luna 储备", "something_new"],
};
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
test(`${engine} ${lang}: a call Codex makes for itself is tagged by its model`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const context = await browser.newContext({ viewport: { width: 1100, height: 760 }, reducedMotion: "reduce" });
const page = await context.newPage();
page.setDefaultTimeout(5000);
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang));
t.after(async () => {
if (process.env.ARTIFACT_DIR) {
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-kind.png`), fullPage: true });
}
await browser.close();
});
await page.goto("http://magpie.test/?view=routing");
await page.locator(".rt-day").nth(1).click();
await page.locator(".rt-req").nth(kinds.length - 1).waitFor();
const tags = await page.locator(".rt-req").evaluateAll((rows) => rows.map((r) => r.querySelector(".asked .kind")?.textContent || ""));
assert.deepEqual(tags, [...want[lang], ""]);
// beside the model, not in its place, the model keeping its room first
const look = await page.locator(".rt-req .asked .kind").first().evaluate((e) => {
const m = e.parentElement.querySelector(".m");
return { after: m.getBoundingClientRect().right <= e.getBoundingClientRect().left + 0.5, shown: m.clientWidth >= m.scrollWidth, model: m.textContent };
});
assert(look.after && look.shown && look.model === "gpt-6-luna", JSON.stringify(look));
// the story says what it was, and the click leaves the row where it is
const row = page.locator(".rt-req").nth(0);
const was = await row.evaluate((e) => e.getBoundingClientRect().top);
await row.click();
await page.waitForTimeout(600);
assert(Math.abs((await row.evaluate((e) => e.getBoundingClientRect().top)) - was) <= 1, "picking the request moved the page");
const story = page.locator(".rt-steps li.kind");
assert.equal(await story.locator(".kind").textContent(), want[lang][0]);
assert.match(await story.textContent(), lang === "zh" ? /不是对话中的一轮/ : /not as a turn of the conversation/);
// a turn has no tag in its story
await page.locator(".rt-req").nth(kinds.length - 1).click();
await page.waitForTimeout(300);
assert.equal(await page.locator(".rt-steps li.kind").count(), 0);
assert.deepEqual(errors, []);
});
}
}
+3
View File
@@ -47,6 +47,9 @@ type Record struct {
// it (X-Magpie-Session, or the session header Claude Code, Codex or
// OpenCode sends): several sessions on one model told apart
Session string `json:"session,omitempty"`
// Kind is what the agent made the call for when it isn't a turn of
// the conversation: a Codex subagent's (review, compact, guardian…)
Kind string `json:"kind,omitempty"`
}
// Path is the log file: ~/.config/magpie/usage.jsonl (XDG-aware).