feat(codex): support backend-authorized Luna Reserve fallback (#1780)

* feat(codex): reconcile Luna Reserve fallback and conditional usage

* fix(codex): preserve queued settings and reconcile Reserve sessions
This commit is contained in:
SSU-WEI HUANG
2026-09-09 09:27:40 +08:00
committed by GitHub
parent 5c5c8b3a9f
commit 8357da0a9d
18 changed files with 928 additions and 21 deletions
+154 -5
View File
@@ -1,8 +1,16 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import type { EnhancedMode } from './loop';
const harness = vi.hoisted(() => ({
appServerConnected: true,
abandonTransport: null as (() => void) | null,
resumeModel: 'gpt-5.4',
reserveUsage: null as Record<string, unknown> | null,
reserveSettingsCalls: [] as Array<Record<string, unknown>>,
notifications: [] as Array<{ method: string; params: unknown }>,
dispatchNotification: null as ((method: string, params: unknown) => void) | null,
registerRequestCalls: [] as string[],
@@ -104,14 +112,14 @@ vi.mock('./codexAppServerClient', () => {
private notificationHandler: ((method: string, params: unknown) => void) | null = null;
private stderrHandler: ((text: string) => void) | null = null;
async connect(): Promise<void> {}
async connect(): Promise<void> { harness.appServerConnected = true; }
isConnected(): boolean {
return true;
return harness.appServerConnected;
}
isInitialized(): boolean {
return true;
return harness.appServerConnected;
}
async initialize(params: unknown): Promise<{ protocolVersion: number }> {
@@ -132,7 +140,7 @@ vi.mock('./codexAppServerClient', () => {
harness.dispatchNotification = handler;
}
setTransportAbandonedHandler(_handler: (() => void) | null): void {}
setTransportAbandonedHandler(handler: (() => void) | null): void { harness.abandonTransport = handler; }
setStderrHandler(handler: ((text: string) => void) | null): void {
this.stderrHandler = handler;
@@ -146,6 +154,25 @@ vi.mock('./codexAppServerClient', () => {
return harness.collaborationModeResponse;
}
async readAccountRateLimits(): Promise<unknown> {
if (!harness.reserveUsage) throw new Error('Unsupported');
return structuredClone(harness.reserveUsage);
}
async supportsMethod(): Promise<boolean> { return true; }
async listModels(): Promise<unknown> {
return { data: [
{ id: 'gpt-reserve', hidden: true, defaultReasoningEffort: 'medium' },
{ id: 'gpt-5.4', hidden: false, defaultReasoningEffort: 'high' }
] };
}
async updateThreadSettings(params: Record<string, unknown>): Promise<void> {
harness.reserveSettingsCalls.push(params);
this.notificationHandler?.('thread/settings/updated', { threadId: params.threadId, threadSettings: params });
}
async listSkills(params: unknown): Promise<unknown> {
harness.listSkillsCalls.push(params);
return harness.skillsListResponse;
@@ -178,7 +205,7 @@ vi.mock('./codexAppServerClient', () => {
if (harness.failResumeThreadIds.includes(id)) {
throw new Error('resume failed');
}
return { thread: { id }, model: 'gpt-5.4' };
return { thread: { id }, model: harness.resumeModel };
}
async compactThread(params?: { threadId?: string }): Promise<Record<string, never>> {
@@ -1109,6 +1136,7 @@ import { INDETERMINATE_SYMBOL } from './codexAppServerClient';
import { RPC_METHODS } from '@hapi/protocol/rpcMethods';
type FakeAgentState = {
codexUsage?: import('@hapi/protocol').AgentState['codexUsage'];
requests: Record<string, unknown>;
completedRequests: Record<string, unknown>;
};
@@ -1204,6 +1232,10 @@ function createSessionStub(
setModelReasoningEffort(nextEffort: EnhancedMode['modelReasoningEffort']) {
currentModelReasoningEffort = nextEffort;
},
getModelReasoningEffort() { return currentModelReasoningEffort; },
pushKeepAlive() {},
getServiceTier() { return undefined; },
setServiceTier() {},
getCollaborationMode() {
return currentCollaborationMode;
},
@@ -1399,6 +1431,11 @@ describe('codexRemoteLauncher', () => {
session.queue.close();
});
afterEach(() => {
harness.appServerConnected = true;
harness.abandonTransport = null;
harness.resumeModel = 'gpt-5.4';
harness.reserveUsage = null;
harness.reserveSettingsCalls = [];
harness.notifications = [];
harness.dispatchNotification = null;
harness.registerRequestCalls = [];
@@ -2658,6 +2695,118 @@ describe('codexRemoteLauncher', () => {
expect(session.thinking).toBe(false);
});
it('preserves queued model settings when resuming an ordinary thread', async () => {
const { session } = createSessionStub(['first'], {
...createMode(), model: 'gpt-5.6', modelReasoningEffort: 'high', serviceTier: 'fast'
}, true);
session.sessionId = 'thread-old';
await codexRemoteLauncher(session as never);
expect(harness.startTurnParams[0]).toMatchObject({
collaborationMode: { settings: { model: 'gpt-5.6', reasoning_effort: 'high' } },
serviceTier: 'priority'
});
});
it.each([
['transient', 'Codex thread entered systemError', 2, false],
['context', "Codex ran out of room in the model's context window.", 2, true],
['usage', 'Usage limit exceeded', 1, false]
] as const)('preserves %s failure handling during Reserve', async (_kind, error, attempts, compact) => {
harness.resumeModel = 'gpt-reserve';
harness.remainingThreadSystemErrors = 1;
harness.nextThreadSystemErrorMessage = error;
harness.reserveUsage = {
accountId: 'account-a', ordinaryUsageAllowed: false,
rateLimits: { limitId: 'codex', primary: { usedPercent: 100 } }
};
const { session } = createSessionStub(['first'], createMode(), true);
session.sessionId = 'thread-reserve';
await codexRemoteLauncher(session as never);
expect(harness.startTurnMessages).toEqual(Array(attempts).fill('first'));
expect(harness.compactThreadIds).toEqual(compact ? ['thread-reserve'] : []);
expect(harness.startThreadIds).toEqual([]);
expect(harness.startTurnParams[0]).toMatchObject({
collaborationMode: { settings: { model: 'gpt-reserve' } }
});
});
it('reconciles a resumed Reserve task before waiting for the first user message', async () => {
harness.resumeModel = 'gpt-reserve';
harness.reserveUsage = {
accountId: 'account-a', ordinaryUsageAllowed: false,
rateLimits: { limitId: 'codex', primary: { usedPercent: 100 } },
rateLimitsByLimitId: { base_model_inference: { limitName: 'gpt-reserve', primary: { usedPercent: 20 } } }
};
const { session, getModel, getAgentState } = createSessionStub([], createMode(), false, false);
session.sessionId = 'thread-reserve';
const run = codexRemoteLauncher(session as never);
try {
await vi.waitFor(() => expect(getAgentState().codexUsage?.reserve?.primary?.remainingPercent).toBe(80));
expect(getModel()).toBe('gpt-5.6-luna');
expect(harness.resumeThreadIds).toEqual(['thread-reserve']);
expect(harness.startTurnMessages).toEqual([]);
} finally {
session.queue.close();
await run;
}
});
it('reads native settings after transport loss without replaying the interrupted message', async () => {
harness.resumeModel = 'gpt-reserve';
harness.suppressTurnCompletion = true;
harness.reserveUsage = {
accountId: 'account-a', ordinaryUsageAllowed: false,
rateLimits: { limitId: 'codex', primary: { usedPercent: 100 } }
};
const { session, getModel, getAgentState } = createSessionStub(['first'], createMode(), false, false);
session.sessionId = 'thread-reserve';
const run = codexRemoteLauncher(session as never);
try {
await vi.waitFor(() => expect(harness.startTurnMessages).toEqual(['first']));
harness.resumeModel = 'gpt-5.4';
harness.reserveUsage.ordinaryUsageAllowed = true;
harness.appServerConnected = false;
harness.abandonTransport?.();
await vi.waitFor(() => expect(harness.resumeThreadIds).toEqual(['thread-reserve', 'thread-reserve']));
await vi.waitFor(() => expect(getAgentState().codexUsage?.reserve).toBeNull());
expect(getModel()).toBe('gpt-5.4');
expect(harness.startTurnMessages).toEqual(['first']);
} finally {
session.queue.close();
await run;
}
});
it('uses accepted Reserve settings for queued turns in the same conversation without replay', async () => {
const home = await mkdtemp(join(tmpdir(), 'hapi-reserve-launcher-'));
vi.stubEnv('CODEX_HOME', home);
try {
harness.reserveUsage = {
accountId: 'account-a', ordinaryUsageAllowed: false,
rateLimits: { limitId: 'codex', primary: { usedPercent: 100 } },
rateLimitsByLimitId: { base_model_inference: { limitName: 'gpt-reserve', primary: { usedPercent: 20 } } },
rateLimitUpsell: { banner_type: 'luna_reserve', blocked_model_slug: 'gpt-5.4' }
};
const { session, getModel, sessionEvents } = createSessionStub(['first', 'second'], createMode(), true);
session.sessionId = '01900000-0000-7000-8000-000000000001';
await codexRemoteLauncher(session as never);
expect(harness.resumeThreadIds).toEqual([session.sessionId]);
expect(harness.resumeThreadParams[0]).not.toHaveProperty('model');
expect(harness.reserveSettingsCalls).toHaveLength(1);
expect(harness.startTurnMessages).toEqual(['first', 'second']);
expect(harness.startTurnParams).toHaveLength(2);
for (const params of harness.startTurnParams) {
expect(params.collaborationMode).toMatchObject({ settings: { model: 'gpt-reserve', reasoning_effort: 'medium' } });
expect(params.threadId).toBe(session.sessionId);
}
expect(getModel()).toBe('gpt-5.6-luna');
expect(sessionEvents.filter(event => String(event.message).includes('Luna Reserve'))).toHaveLength(1);
} finally {
vi.unstubAllEnvs();
await rm(home, { recursive: true, force: true });
}
});
it('does not create a new thread when an existing conversation cannot be resumed', async () => {
harness.failResumeThreadIds = ['thread-old'];
const { session, sessionEvents } = createSessionStub(['first message']);