openai / openai/codex

app-server clients cannot use Luna Reserve: supportsLunaReserve exists but has no accept/redeem action

Open
#45,132 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server bug rate-limits
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex CLI is running?

codex-cli 0.154.0 (standalone build at ~/.codex/packages/standalone/current/bin/codex)

What subscription do you have?

ChatGPT Plus

Which model were you using?

gpt-5.6-luna (also reproduced by running gpt-5.6-sol first, then retrying gpt-5.6-luna on the same thread)

What platform is your computer?

Darwin 23.6.0 arm64 arm (macOS 14.8.7)

What terminal emulator and version are you using (if applicable)?

N/A. This is driven headlessly through codex app-server over stdio JSON-RPC (from a third-party client: Paseo daemon 0.8.0).

Codex doctor report

not available

What issue are you seeing?

Third-party clients that talk to codex app-server cannot use the Luna Reserve fallback after the ordinary usage limit is exhausted, even though the account is eligible and the reserve pool is visible over the same protocol.

account/rateLimits/read returns all of the following:

{
  "ordinaryUsageAllowed": false,
  "rateLimitsByLimitId": {
    "codex": { "primary": { "usedPercent": 100, "windowDurationMins": 300 } },
    "base_model_inference": {
      "limitId": "base_model_inference",
      "limitName": "gpt-reserve",
      "normalModelSlug": "gpt-5.6-luna",
      "primary": { "usedPercent": 6, "windowDurationMins": 10080 }
    }
  },
  "rateLimitUpsell": { "banner_type": "luna_reserve", "title": "You're now using Luna, a faster model for simpler tasks." }
}

So the ordinary codex 5h window is exhausted, but the gpt-reserve pool (which maps to gpt-5.6-luna) is only 6% used and a luna_reserve banner is offered. Nevertheless a turn fails:

error: You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit
       https://chatgpt.com/codex/settings/usage to purchase more credits or try again at 10:58 AM.
codexErrorInfo: "usageLimitExceeded"
turn/completed status: "failed"

Setting supportsLunaReserve: true on account/rateLimits/read does not change this. The interactive TUI works for the same account, so the entitlement exists; only app-server clients are blocked.

What steps can reproduce the bug?
  1. Confirm the protocol surface (only one Luna-related field exists, and there is no accept/redeem method):
codex app-server generate-json-schema --out /tmp/gen
grep -r supportsLunaReserve /tmp/gen       # only GetAccountRateLimitsParams
  1. Spawn codex app-server (stdio) and drive it with JSON-RPC:
initialize   { clientInfo: {name:"codex_app_server_daemon",title:"Codex App Server Daemon",version:"0.0.0"},
               capabilities: { experimentalApi: true } }
initialized  {}
account/rateLimits/read { supportsLunaReserve: true }   # returns luna_reserve banner + gpt-reserve pool
thread/start { model: "gpt-5.6-luna", cwd: "/tmp" }
turn/start   { threadId, input: [{type:"text", text:"Reply with exactly OK and nothing else."}] }
  1. Observe the error notification with codexErrorInfo: "usageLimitExceeded" followed by turn/completed with status: "failed".

Minimal repro (Node, ~40 lines) — line-delimited JSON-RPC over stdio:

import { spawn } from "node:child_process";
const child = spawn(process.env.HOME + "/.codex/packages/standalone/current/bin/codex", ["app-server"], { stdio: ["pipe","pipe","pipe"] });
let id = 0, buf = ""; const pending = new Map();
const send = (method, params) => { const i = ++id; child.stdin.write(JSON.stringify({ id: i, method, params }) + "\n");
  return new Promise((res, rej) => pending.set(i, { res, rej })); };
const notify = (method, params) => child.stdin.write(JSON.stringify({ method, params }) + "\n");
child.stdout.on("data", (d) => { buf += d; let i;
  while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); if (!line) continue;
    const m = JSON.parse(line);
    if (m.id !== undefined && (m.result !== undefined || m.error)) { const p = pending.get(m.id); if (p) { pending.delete(m.id); m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result); } }
    else if (m.id !== undefined && m.method) child.stdin.write(JSON.stringify({ id: m.id, result: {} }) + "\n");
    else if (m.method === "error" || m.method === "turn/completed") console.log(m.method, JSON.stringify(m.params).slice(0, 300));
  }
});
const main = async () => {
  await send("initialize", { clientInfo: { name: "codex_app_server_daemon", title: "Codex App Server Daemon", version: "0.0.0" }, capabilities: { experimentalApi: true } });
  notify("initialized", {});
  const rl = await send("account/rateLimits/read", { supportsLunaReserve: true });
  console.log("ordinaryUsageAllowed:", rl.ordinaryUsageAllowed, "| banner:", rl.rateLimitUpsell?.banner_type,
              "| reserve used%:", rl.rateLimitsByLimitId?.base_model_inference?.primary?.usedPercent);
  const ts = await send("thread/start", { model: "gpt-5.6-luna", cwd: "/tmp" });
  await send("turn/start", { threadId: ts.thread.id, input: [{ type: "text", text: "Reply with exactly OK and nothing else." }] });
};
main();

I also tried declaring supportsLunaReserve: true, running a blocked gpt-5.6-sol turn, and then retrying gpt-5.6-luna on the same thread and connection. It still fails with usageLimitExceeded.

What is the expected behavior?

One of:

  • The app-server protocol should expose the same "Continue with Luna Reserve" recovery the TUI performs (i.e. an RPC to accept/redeem the reserve, or a way to mark a turn/thread as a reserve continuation), or
  • supportsLunaReserve: true should actually enable the server-side fallback for subsequent model requests.

Right now supportsLunaReserve is documented as "allow the backend to record experiment exposure after ordinary usage is blocked", and there is no corresponding action in the protocol. The whole flow appears to live only in codex_tui:

codex_tui::chatwidget::backend_banners::ChatWidget::prepare_luna_reserve_return
codex_tui::chatwidget::backend_banners::ChatWidget::defer_pending_turn_for_luna_reserve
codex_tui::chatwidget::backend_banners::ChatWidget::apply_reserve_fallback_to_pending_turn
codex_tui::chatwidget::backend_banners::ChatWidget::show_unavailable_reserve_recovery

plus the header x-openai-codex-luna-reserve, the state directory ~/.codex/tui-luna-reserve/, and error code luna-reserve-recovery — with no equivalent core/app-server entry point.

Additional information
  • Related: #40939 (CLI cannot use Luna Reserve), #41520 (Cannot use gpt-reserve), #42153 (Desktop manual Luna switch stays blocked), #41224.
  • Practically, this blocks any integrator that embeds Codex via codex app-server (Paseo, IDE integrations, custom UIs) from using a reserve the account already has, forcing users back to the interactive TUI.
  • Console login codex login uses the same ChatGPT Plus account; not a multi-account or workspace-owner issue.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the app-server protocol surface around account/rateLimits/read, thread/start, and turn/start, then compare it with the named codex_tui Luna Reserve handlers. Check the generated schema for the missing recovery action and trace how the TUI uses the reserve state and error code. Done means an app-server client can invoke or trigger the same reserve recovery and complete a turn.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, rust
Domain
api, backend-api-design
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.