feat: adapter model discovery, reasoning effort, and improved codex formatting
Add dynamic OpenAI model list fetching for codex adapter with caching, async listModels interface, reasoning effort support for both claude and codex adapters, optional timeouts (default to unlimited), wakeCommentId context propagation, and richer codex stdout event parsing/formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { isCodexUnknownSessionError, parseCodexJsonl } from "@paperclip/adapter-codex-local/server";
|
||||
import { parseCodexStdoutLine } from "@paperclip/adapter-codex-local/ui";
|
||||
import { printCodexStreamEvent } from "@paperclip/adapter-codex-local/cli";
|
||||
|
||||
describe("codex_local parser", () => {
|
||||
it("extracts session, summary, usage, and terminal error message", () => {
|
||||
@@ -30,3 +32,220 @@ describe("codex_local stale session detection", () => {
|
||||
expect(isCodexUnknownSessionError("", stderr)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("codex_local ui stdout parser", () => {
|
||||
it("parses turn and reasoning lifecycle events", () => {
|
||||
const ts = "2026-02-20T00:00:00.000Z";
|
||||
|
||||
expect(parseCodexStdoutLine(JSON.stringify({ type: "turn.started" }), ts)).toEqual([
|
||||
{ kind: "system", ts, text: "turn started" },
|
||||
]);
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: { id: "item_1", type: "reasoning", text: "**Preparing to use paperclip skill**" },
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{ kind: "thinking", ts, text: "**Preparing to use paperclip skill**" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses command execution and file changes", () => {
|
||||
const ts = "2026-02-20T00:00:00.000Z";
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "item.started",
|
||||
item: { id: "item_2", type: "command_execution", command: "/bin/zsh -lc ls", status: "in_progress" },
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: "command_execution",
|
||||
input: { id: "item_2", command: "/bin/zsh -lc ls" },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "item_2",
|
||||
type: "command_execution",
|
||||
command: "/bin/zsh -lc ls",
|
||||
aggregated_output: "agents\n",
|
||||
exit_code: 0,
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId: "item_2",
|
||||
content: "command: /bin/zsh -lc ls\nstatus: completed\nexit_code: 0\n\nagents",
|
||||
isError: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "item_52",
|
||||
type: "file_change",
|
||||
changes: [{ path: "/Users/genericuser/paperclip/ui/src/pages/AgentDetail.tsx", kind: "update" }],
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "system",
|
||||
ts,
|
||||
text: "file changes: update /Users/genericuser/paperclip/ui/src/pages/AgentDetail.tsx",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses error items and failed turns", () => {
|
||||
const ts = "2026-02-20T00:00:00.000Z";
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "item_0",
|
||||
type: "error",
|
||||
message: "This session was recorded with model `gpt-5.2-pro` but is resuming with `gpt-5.2-codex`.",
|
||||
},
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "stderr",
|
||||
ts,
|
||||
text: "This session was recorded with model `gpt-5.2-pro` but is resuming with `gpt-5.2-codex`.",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
parseCodexStdoutLine(
|
||||
JSON.stringify({
|
||||
type: "turn.failed",
|
||||
error: { message: "model access denied" },
|
||||
usage: { input_tokens: 10, cached_input_tokens: 2, output_tokens: 4 },
|
||||
}),
|
||||
ts,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: "",
|
||||
inputTokens: 10,
|
||||
outputTokens: 4,
|
||||
cachedTokens: 2,
|
||||
costUsd: 0,
|
||||
subtype: "turn.failed",
|
||||
isError: true,
|
||||
errors: ["model access denied"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function stripAnsi(value: string): string {
|
||||
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
describe("codex_local cli formatter", () => {
|
||||
it("prints lifecycle, command execution, file change, and error events", () => {
|
||||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
printCodexStreamEvent(JSON.stringify({ type: "turn.started" }), false);
|
||||
printCodexStreamEvent(
|
||||
JSON.stringify({
|
||||
type: "item.started",
|
||||
item: { id: "item_2", type: "command_execution", command: "/bin/zsh -lc ls", status: "in_progress" },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
printCodexStreamEvent(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "item_2",
|
||||
type: "command_execution",
|
||||
command: "/bin/zsh -lc ls",
|
||||
aggregated_output: "agents\n",
|
||||
exit_code: 0,
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
printCodexStreamEvent(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "item_52",
|
||||
type: "file_change",
|
||||
changes: [{ path: "/Users/genericuser/paperclip/ui/src/pages/AgentDetail.tsx", kind: "update" }],
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
printCodexStreamEvent(
|
||||
JSON.stringify({
|
||||
type: "turn.failed",
|
||||
error: { message: "model access denied" },
|
||||
usage: { input_tokens: 10, cached_input_tokens: 2, output_tokens: 4 },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
printCodexStreamEvent(
|
||||
JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: { type: "error", message: "resume model mismatch" },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
const lines = spy.mock.calls
|
||||
.map((call) => call.map((v) => String(v)).join(" "))
|
||||
.map(stripAnsi);
|
||||
|
||||
expect(lines).toEqual(expect.arrayContaining([
|
||||
"turn started",
|
||||
"tool_call: command_execution",
|
||||
"/bin/zsh -lc ls",
|
||||
"tool_result: command_execution command=\"/bin/zsh -lc ls\" status=completed exit_code=0",
|
||||
"agents",
|
||||
"file_change: update /Users/genericuser/paperclip/ui/src/pages/AgentDetail.tsx",
|
||||
"turn failed: model access denied",
|
||||
"tokens: in=10 out=4 cached=2",
|
||||
"error: resume model mismatch",
|
||||
]));
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user