fix: preserve usage model and local dates

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-08-03 17:00:14 +08:00
co-authored by HAPI
parent 41f172df26
commit 5fd0e839fd
22 changed files with 208 additions and 41 deletions
+15 -1
View File
@@ -89,10 +89,11 @@ describe('convertAgentMessage', () => {
totalTokens: 13_892,
contextTokens: 13_879,
contextWindow: 65_536
});
}, 'kimi-k2.5');
expect(converted).toEqual({
type: 'token_count',
model: 'kimi-k2.5',
info: {
total: {
inputTokens: 8119,
@@ -106,6 +107,19 @@ describe('convertAgentMessage', () => {
}
});
});
it('stamps unknown usage models explicitly', () => {
const converted = convertAgentMessage({
type: 'usage',
inputTokens: 10,
outputTokens: 2
});
expect(converted).toMatchObject({
type: 'token_count',
model: null
});
});
it('returns null instead of echoing an unrecognized message shape', () => {
// Unreachable through the type system, but callers forward any non-null
// result straight into the chat stream — so the runtime contract has to
+3 -1
View File
@@ -6,6 +6,7 @@ export type CodexMessage =
| { type: 'reasoning'; message: string; id: string }
| {
type: 'token_count';
model: string | null;
info: {
total: {
inputTokens: number;
@@ -36,7 +37,7 @@ export type CodexMessage =
| { type: 'plan'; entries: PlanItem[] }
| { type: 'error'; message: string };
export function convertAgentMessage(message: AgentMessage): CodexMessage | null {
export function convertAgentMessage(message: AgentMessage, model?: string | null): CodexMessage | null {
switch (message.type) {
case 'text':
return { type: 'message', message: message.text };
@@ -48,6 +49,7 @@ export function convertAgentMessage(message: AgentMessage): CodexMessage | null
case 'usage':
return {
type: 'token_count',
model: typeof model === 'string' && model.trim() ? model.trim() : null,
info: {
total: {
inputTokens: message.inputTokens,
+2 -1
View File
@@ -188,7 +188,8 @@ export async function runAgentSession(opts: {
try {
await backend.prompt(agentSessionId, promptContent, (message) => {
const converted = convertAgentMessage(message);
const model = backend.getSessionModelsMetadata?.(agentSessionId)?.currentModelId;
const converted = convertAgentMessage(message, model);
if (converted) {
session.sendAgentMessage(converted);
}
+10 -6
View File
@@ -373,9 +373,9 @@ describe('codexLocalLauncher', () => {
});
});
it('tracks explicit and default reasoning effort from local turn context', async () => {
it('tracks local turn context and stamps its model on usage', async () => {
const transcriptPath = await writeTranscriptMeta('codex-turn-context.jsonl', 'codex-thread-effort');
const { session, getModelReasoningEffort, getModelReasoningEffortUpdates } = createSessionStub('default');
const { session, agentMessages, getModelReasoningEffort, getModelReasoningEffortUpdates } = createSessionStub('default');
let releaseRunBarrier: (() => void) | undefined;
harness.runBarrier = new Promise((resolve) => {
releaseRunBarrier = resolve;
@@ -393,13 +393,13 @@ describe('codexLocalLauncher', () => {
type: 'turn_context',
payload: { effort: 'max' }
}),
JSON.stringify({
type: 'event_msg',
payload: { type: 'token_count', info: {} }
}),
JSON.stringify({
type: 'turn_context',
payload: { model: 'gpt-5.4' }
}),
JSON.stringify({
type: 'event_msg',
payload: { type: 'token_count', info: {} }
})
].join('\n') + '\n');
await wait(700);
@@ -409,6 +409,10 @@ describe('codexLocalLauncher', () => {
expect(getModelReasoningEffortUpdates()).toEqual(['max', null]);
expect(getModelReasoningEffort()).toBeNull();
expect(agentMessages).toContainEqual(expect.objectContaining({
type: 'token_count',
model: 'gpt-5.4'
}));
});
it('renders nested Code Mode plans and commands without their covered exec wrapper', async () => {
+18 -2
View File
@@ -36,6 +36,16 @@ function extractTurnContextReasoningEffort(event: CodexSessionEvent): ReasoningE
return effort.trim().toLowerCase();
}
function extractTurnContextModel(event: CodexSessionEvent): string | null | undefined {
if (event.type !== 'turn_context' || !event.payload || typeof event.payload !== 'object') {
return undefined;
}
const model = (event.payload as Record<string, unknown>).model;
if (model === null) return null;
if (typeof model !== 'string' || !model.trim()) return undefined;
return model.trim();
}
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
const resumeSessionId = session.sessionId;
let primarySessionId = resumeSessionId;
@@ -47,6 +57,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
let transcriptLocator: CodexTranscriptLocator | null = null;
let scannerTranscriptPath: string | null = null;
let scannerReplayedExistingHistory = false;
let transcriptModel: string | null = null;
const pendingPlansByTurnId = new Map<string, ProposedPlanMessage>();
const pendingExecWrappers = new Map<string, PendingExecWrapper>();
const toolHookBridge = new CodexToolHookBridge();
@@ -203,6 +214,10 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
session.onSessionFound(sessionId);
},
onEvent: (event, context) => {
const observedModel = extractTurnContextModel(event);
if (observedModel !== undefined) {
transcriptModel = observedModel;
}
const observedReasoningEffort = extractTurnContextReasoningEffort(event);
if (observedReasoningEffort !== undefined) {
session.setModelReasoningEffort(observedReasoningEffort);
@@ -245,15 +260,16 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
const scopedMessage = message.type !== 'token_count'
? message
: context.replayedHistory
? { ...message, hapiUsageScope: 'imported-history' }
? { ...message, model: transcriptModel, hapiUsageScope: 'imported-history' }
: primarySessionId
? {
...message,
model: transcriptModel,
threadId: primarySessionId,
thread_id: primarySessionId,
hapiUsageScope: 'managed'
}
: message;
: { ...message, model: transcriptModel };
session.sendAgentMessage(scopedMessage);
}
}
+5
View File
@@ -681,6 +681,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
let scheduleReadyAfterTurn: (() => void) | null = null;
let clearReadyAfterTurnTimer: (() => void) | null = null;
let turnInFlight = false;
let usageModel: string | null = null;
let allowAnonymousTerminalEvent = false;
let invalidThreadId: string | null = null;
let childAgentActivityInCurrentTurn = false;
@@ -2874,6 +2875,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
const threadId = eventThreadId ?? this.currentThreadId;
session.sendAgentMessage({
...addCodexEventScope(msg, 'parent', threadId),
model: asString(msg.model) ?? usageModel,
id: randomUUID()
});
}
@@ -3784,6 +3786,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
...message.mode,
model: session.getModel() ?? message.mode.model
};
usageModel = typeof mode.model === 'string' && mode.model.trim()
? mode.model.trim()
: null;
const shouldSendCollaborationMode = supportsTurnCollaborationMode
&& Boolean(mode.collaborationMode);
const clientUserMessageId = message.items
+1 -1
View File
@@ -458,7 +458,7 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
}
private handleAgentMessage(message: AgentMessage): void {
const converted = convertAgentMessage(message);
const converted = convertAgentMessage(message, this.currentBackendModel);
if (converted) {
this.session.sendAgentMessage(converted);
}
+1 -1
View File
@@ -195,7 +195,7 @@ class CursorRemoteLauncher extends RemoteLauncherBase {
} else if (event.type === 'assistant' || event.type === 'tool_call' || event.type === 'result') {
const agentMsg = convertCursorEventToAgentMessage(event);
if (agentMsg) {
const codexMsg = convertAgentMessage(agentMsg);
const codexMsg = convertAgentMessage(agentMsg, session.model);
if (codexMsg) {
session.sendAgentMessage(codexMsg);
}
+1 -1
View File
@@ -384,7 +384,7 @@ class GrokRemoteLauncher extends RemoteLauncherBase {
}
private handleAgentMessage(message: AgentMessage): void {
const converted = convertAgentMessage(message)
const converted = convertAgentMessage(message, this.currentBackendModel)
if (converted) this.session.sendAgentMessage(converted)
switch (message.type) {
+1 -1
View File
@@ -42,7 +42,7 @@ export async function kimiLocalLauncher(
if (shuttingDown) {
return;
}
const converted = convertKimiWireEvent(event);
const converted = convertKimiWireEvent(event, session.getModel() ?? opts.model);
if (!converted) {
return;
}
+1 -1
View File
@@ -222,7 +222,7 @@ class KimiRemoteLauncher extends RemoteLauncherBase {
}
private handleAgentMessage(message: AgentMessage): void {
const converted = convertAgentMessage(message);
const converted = convertAgentMessage(message, this.currentBackendModel);
if (converted) {
this.session.sendAgentMessage(converted);
}
+2 -1
View File
@@ -77,9 +77,10 @@ describe('convertKimiWireEvent', () => {
uuid: 's1',
usage: { inputOther: 100, output: 20, inputCacheRead: 50, inputCacheCreation: 10 }
}
})).toEqual({
}, 'kimi-k2.5')).toEqual({
message: {
type: 'token_count',
model: 'kimi-k2.5',
info: { total: { inputTokens: 160, outputTokens: 20, cachedInputTokens: 50 } }
}
});
+2 -1
View File
@@ -65,7 +65,7 @@ function extractInputText(input: unknown): string | null {
* Everything else (metadata, config.update, llm.request, usage.record,
* step.begin, plan_mode.*, …) is ignored.
*/
export function convertKimiWireEvent(event: KimiWireEvent): KimiWireConversion | null {
export function convertKimiWireEvent(event: KimiWireEvent, model?: string | null): KimiWireConversion | null {
if (event.type === 'turn.prompt' || event.type === 'turn.steer') {
const origin = asRecord(event.origin);
if (asString(origin?.kind) !== 'user') {
@@ -149,6 +149,7 @@ export function convertKimiWireEvent(event: KimiWireEvent): KimiWireConversion |
return {
message: {
type: 'token_count',
model: typeof model === 'string' && model.trim() ? model.trim() : null,
info: {
total: {
inputTokens: inputOther + cacheRead + cacheCreation,
+1 -1
View File
@@ -767,7 +767,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
}
private handleAgentMessage(message: AgentMessage): void {
const converted = convertAgentMessage(message);
const converted = convertAgentMessage(message, this.currentBackendModel);
if (converted) {
this.session.sendAgentMessage(converted);
}
+3 -3
View File
@@ -326,7 +326,7 @@ async function publishPiTurnUsage(
const usageMessage = convertPiTurnUsage(event, contextUsage);
if (!usageMessage) return;
const converted = convertAgentMessage(usageMessage);
const converted = convertAgentMessage(usageMessage, session.currentModel);
if (converted) session.sendAgentMessage(converted);
}
@@ -362,7 +362,7 @@ export function wireTransportEvents(
const accumulated = assistantMessageAccumulator.handleEvent(event);
if (accumulated.length > 0) {
for (const msg of accumulated) {
const converted = convertAgentMessage(msg);
const converted = convertAgentMessage(msg, session.currentModel);
if (converted) session.sendAgentMessage(converted);
}
}
@@ -371,7 +371,7 @@ export function wireTransportEvents(
if (event.type !== 'message_start' && event.type !== 'message_update' && event.type !== 'message_end') {
const messages = convertPiEvent(event);
for (const msg of messages) {
const converted = convertAgentMessage(msg);
const converted = convertAgentMessage(msg, session.currentModel);
if (converted) session.sendAgentMessage(converted);
}
}