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:
@@ -13,61 +13,240 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function asString(value: unknown, fallback = ""): string {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown, fallback = 0): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function errorText(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
const rec = asRecord(value);
|
||||
if (!rec) return "";
|
||||
const msg =
|
||||
(typeof rec.message === "string" && rec.message) ||
|
||||
(typeof rec.error === "string" && rec.error) ||
|
||||
(typeof rec.code === "string" && rec.code) ||
|
||||
"";
|
||||
if (msg) return msg;
|
||||
try {
|
||||
return JSON.stringify(rec);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyUnknown(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
if (value === null || value === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function parseCommandExecutionItem(
|
||||
item: Record<string, unknown>,
|
||||
ts: string,
|
||||
phase: "started" | "completed",
|
||||
): TranscriptEntry[] {
|
||||
const id = asString(item.id);
|
||||
const command = asString(item.command);
|
||||
const status = asString(item.status);
|
||||
const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null;
|
||||
const output = asString(item.aggregated_output).replace(/\s+$/, "");
|
||||
|
||||
if (phase === "started") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: "command_execution",
|
||||
input: {
|
||||
id,
|
||||
command,
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (command) lines.push(`command: ${command}`);
|
||||
if (status) lines.push(`status: ${status}`);
|
||||
if (exitCode !== null) lines.push(`exit_code: ${exitCode}`);
|
||||
if (output) {
|
||||
if (lines.length > 0) lines.push("");
|
||||
lines.push(output);
|
||||
}
|
||||
|
||||
const isError =
|
||||
(exitCode !== null && exitCode !== 0) ||
|
||||
status === "failed" ||
|
||||
status === "errored" ||
|
||||
status === "error" ||
|
||||
status === "cancelled";
|
||||
|
||||
return [{
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId: id || command || "command_execution",
|
||||
content: lines.join("\n").trim() || "command completed",
|
||||
isError,
|
||||
}];
|
||||
}
|
||||
|
||||
function parseFileChangeItem(item: Record<string, unknown>, ts: string): TranscriptEntry[] {
|
||||
const changes = Array.isArray(item.changes) ? item.changes : [];
|
||||
const entries = changes
|
||||
.map((changeRaw) => asRecord(changeRaw))
|
||||
.filter((change): change is Record<string, unknown> => Boolean(change))
|
||||
.map((change) => {
|
||||
const kind = asString(change.kind, "update");
|
||||
const path = asString(change.path, "unknown");
|
||||
return `${kind} ${path}`;
|
||||
});
|
||||
|
||||
if (entries.length === 0) {
|
||||
return [{ kind: "system", ts, text: "file changes applied" }];
|
||||
}
|
||||
|
||||
const preview = entries.slice(0, 6).join(", ");
|
||||
const more = entries.length > 6 ? ` (+${entries.length - 6} more)` : "";
|
||||
return [{ kind: "system", ts, text: `file changes: ${preview}${more}` }];
|
||||
}
|
||||
|
||||
function parseCodexItem(
|
||||
item: Record<string, unknown>,
|
||||
ts: string,
|
||||
phase: "started" | "completed",
|
||||
): TranscriptEntry[] {
|
||||
const itemType = asString(item.type);
|
||||
|
||||
if (itemType === "agent_message") {
|
||||
const text = asString(item.text);
|
||||
if (text) return [{ kind: "assistant", ts, text }];
|
||||
return [];
|
||||
}
|
||||
|
||||
if (itemType === "reasoning") {
|
||||
const text = asString(item.text);
|
||||
if (text) return [{ kind: "thinking", ts, text }];
|
||||
return [{ kind: "system", ts, text: phase === "started" ? "reasoning started" : "reasoning completed" }];
|
||||
}
|
||||
|
||||
if (itemType === "command_execution") {
|
||||
return parseCommandExecutionItem(item, ts, phase);
|
||||
}
|
||||
|
||||
if (itemType === "file_change" && phase === "completed") {
|
||||
return parseFileChangeItem(item, ts);
|
||||
}
|
||||
|
||||
if (itemType === "tool_use") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: asString(item.name, "unknown"),
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
}
|
||||
|
||||
if (itemType === "tool_result" && phase === "completed") {
|
||||
const toolUseId = asString(item.tool_use_id, asString(item.id));
|
||||
const content =
|
||||
asString(item.content) ||
|
||||
asString(item.output) ||
|
||||
asString(item.result) ||
|
||||
stringifyUnknown(item.content ?? item.output ?? item.result);
|
||||
const isError = item.is_error === true || asString(item.status) === "error";
|
||||
return [{ kind: "tool_result", ts, toolUseId, content, isError }];
|
||||
}
|
||||
|
||||
if (itemType === "error" && phase === "completed") {
|
||||
const text = errorText(item.message ?? item.error ?? item);
|
||||
return [{ kind: "stderr", ts, text: text || "error" }];
|
||||
}
|
||||
|
||||
const id = asString(item.id);
|
||||
const status = asString(item.status);
|
||||
const meta = [id ? `id=${id}` : "", status ? `status=${status}` : ""].filter(Boolean).join(" ");
|
||||
return [{ kind: "system", ts, text: `item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}` }];
|
||||
}
|
||||
|
||||
export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[] {
|
||||
const parsed = asRecord(safeJsonParse(line));
|
||||
if (!parsed) {
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
|
||||
const type = typeof parsed.type === "string" ? parsed.type : "";
|
||||
const type = asString(parsed.type);
|
||||
|
||||
if (type === "thread.started") {
|
||||
const threadId = typeof parsed.thread_id === "string" ? parsed.thread_id : "";
|
||||
const threadId = asString(parsed.thread_id);
|
||||
return [{
|
||||
kind: "init",
|
||||
ts,
|
||||
model: "codex",
|
||||
model: asString(parsed.model, "codex"),
|
||||
sessionId: threadId,
|
||||
}];
|
||||
}
|
||||
|
||||
if (type === "item.completed") {
|
||||
if (type === "turn.started") {
|
||||
return [{ kind: "system", ts, text: "turn started" }];
|
||||
}
|
||||
|
||||
if (type === "item.started" || type === "item.completed") {
|
||||
const item = asRecord(parsed.item);
|
||||
if (item) {
|
||||
const itemType = typeof item.type === "string" ? item.type : "";
|
||||
if (itemType === "agent_message") {
|
||||
const text = typeof item.text === "string" ? item.text : "";
|
||||
if (text) return [{ kind: "assistant", ts, text }];
|
||||
}
|
||||
if (itemType === "tool_use") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: typeof item.name === "string" ? item.name : "unknown",
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
}
|
||||
}
|
||||
if (!item) return [{ kind: "system", ts, text: type.replace(".", " ") }];
|
||||
return parseCodexItem(item, ts, type === "item.started" ? "started" : "completed");
|
||||
}
|
||||
|
||||
if (type === "turn.completed") {
|
||||
const usage = asRecord(parsed.usage) ?? {};
|
||||
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
||||
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
||||
const cachedTokens = typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : 0;
|
||||
const usage = asRecord(parsed.usage);
|
||||
const inputTokens = asNumber(usage?.input_tokens);
|
||||
const outputTokens = asNumber(usage?.output_tokens);
|
||||
const cachedTokens = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
|
||||
return [{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: "",
|
||||
text: asString(parsed.result),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cachedTokens,
|
||||
costUsd: 0,
|
||||
subtype: "",
|
||||
isError: false,
|
||||
errors: [],
|
||||
costUsd: asNumber(parsed.total_cost_usd),
|
||||
subtype: asString(parsed.subtype),
|
||||
isError: parsed.is_error === true,
|
||||
errors: Array.isArray(parsed.errors)
|
||||
? parsed.errors.map(errorText).filter(Boolean)
|
||||
: [],
|
||||
}];
|
||||
}
|
||||
|
||||
if (type === "turn.failed") {
|
||||
const usage = asRecord(parsed.usage);
|
||||
const inputTokens = asNumber(usage?.input_tokens);
|
||||
const outputTokens = asNumber(usage?.output_tokens);
|
||||
const cachedTokens = asNumber(usage?.cached_input_tokens, asNumber(usage?.cache_read_input_tokens));
|
||||
const message = errorText(parsed.error ?? parsed.message);
|
||||
return [{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: asString(parsed.result),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cachedTokens,
|
||||
costUsd: asNumber(parsed.total_cost_usd),
|
||||
subtype: asString(parsed.subtype, "turn.failed"),
|
||||
isError: true,
|
||||
errors: message ? [message] : [],
|
||||
}];
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
const message = errorText(parsed.message ?? parsed.error ?? parsed);
|
||||
return [{ kind: "stderr", ts, text: message || line }];
|
||||
}
|
||||
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user