Skip to content

[Bug]: Shadow Call Intercept forces effort low on every gpt-5.6-luna request when luna is the target (or main model) — max turns silently downgraded #2706

Closed

Description

@keohanoi

Client or integration

Codex CLI

Area

Proxy and routing

Summary

gpt-5.6-luna selected with [max] reasoning returns near-instant, shallow responses through the proxy, while the same model/effort directly against OpenAI thinks for a long time. Root cause: Shadow Call Intercept was enabled with gpt-5.6-luna itself as the target model, and the intercept's slug-only matching treats every luna request — main turns included — as a helper call, forcing reasoning.effort to low on the wire.

Mechanism (v2.33.0):

  1. shouldInterceptShadowCall (src/lib/shadow-call.ts) matches the model slug alone, unconditionally (deliberate since In Codex 0.147.0, background helper requests use gpt-5.6-luna. Even with OpenCodex's Shadow Call Intercept enabled, some Luna requests still reached OpenAI. #1684 — helper calls cannot be distinguished by headers). gpt-5.6-luna is both the helper slug and a legitimate main model.
  2. The rewrite in src/server/responses/core.ts (~2306–2330) replaces the model and forces effort: parsed.options.reasoning = "low" + _rawBody.reasoning = { effort: "low" }. When the configured target is luna (self-target), the model rewrite is a no-op but the effort clamp still applies to every request. Self-interception is pure loss.
  3. logCtx.requestedEffort is captured at ~2296, before the rewrite — so usage.jsonl / the Logs page show requestedEffort: "max" while the wire effort was low. Unlike applyEffortCap (which annotates from->to), the shadow rewrite leaves the log stale, which makes this very hard to diagnose from logs.

Evidence from usage.jsonl (incident window Aug 26 20:29–22:21 local, 258 consecutive requests): every entry has resolvedModel: "gpt-5.6-luna", provider: "openai", requestedEffort: "max", and shadowCallRewrittenFrom: "gpt-5.6-luna". reasoningOutputTokens on those max turns: 13–31 (median ~24). Earlier non-intercepted max turns on the same setup reach 5,000+ reasoning tokens and 30+ minute turns. The four hard turns the user actually noticed were all inside the window.

Four concrete defects:

  1. No self-target guard (runtime): shadowCallIntercept.model matching a shadow source model is accepted and silently degrades every request for that model to low. The management API (/api/shadow-call-settings) validates shape but not self-intersection.
  2. Slug-only matching hijacks luna main turns (design gap): with any target configured, a user whose main model is luna gets all main turns rerouted at low effort. [Bug]: Codex App now sends gpt-5.6-luna helper requests on every message and turn completion (previously only title generation) #2157 established Codex App emits luna helper calls on every turn, so operators are encouraged to enable the intercept — but nothing warns that this is incompatible with luna as a main model.
  3. GUI offers the source model as target: shadowCallModelOptions (gui/src/pages/dashboard-shared.ts:383) lists every active model, including openai/gpt-5.6-luna itself. That is exactly how this config was reached from the dashboard.
  4. Misleading logs: requestedEffort is recorded pre-rewrite (see above).

Suggested fixes: reject (or skip at runtime) a target that matches a shadow source model; exclude shadow source models from the target dropdown; annotate the effort rewrite in the request log (max->low) like applyEffortCap does; document/warn that enabling the intercept while luna is the main model downgrades main turns.

Reproduction

  1. ocx start with the built-in openai forward provider logged in (2.33.0).
  2. Dashboard → Models → enable Shadow Call Intercept; in the target dropdown pick openai/gpt-5.6-luna (it is offered).
  3. In Codex CLI, select gpt-5.6-luna [max] as the main model and send a genuinely hard task (one that normally thinks for minutes).
  4. Observe: responses return in seconds with shallow reasoning. Direct OpenAI with the same model/effort on the same task thinks for a long time.
  5. Inspect ~/.opencodex/usage.jsonl: every turn carries shadowCallRewrittenFrom: "gpt-5.6-luna" and requestedEffort: "max", with reasoningOutputTokens in the tens.

Variant of step 2 with any other target (e.g. zai/glm-5.3-flash) still reproduces the user-visible problem for luna-main users: every main turn is rerouted to the target model at low effort (per the unconditional matching introduced for #1684).

Version

2.33.0

Operating system

macOS 26.5.1

Provider and model

openai / gpt-5.6-luna

Logs or error output

# usage.jsonl entries during the incident window (aggregate; identifying fields removed)
# all 258 requests in the window, including the user's [max] main turns:
{
  "provider": "openai",
  "resolvedModel": "gpt-5.6-luna",
  "requestedModel": "gpt-5.6-luna",
  "shadowCallRewrittenFrom": "gpt-5.6-luna",
  "requestedEffort": "max",          # captured BEFORE the rewrite — misleading
  "status": 200,
  "durationMs": 9396,
  "usage": { "inputTokens": 194311, "outputTokens": 101,
             "reasoningOutputTokens": 13 }   # wire effort was "low"
}

# earlier non-intercepted [max] turns, same provider/model/setup:
#   reasoningOutputTokens up to 5083, durationMs up to 2203997 (~37 min)

Screenshots and supporting files

n/a

Redacted configuration

{
  "defaultProvider": "openai",
  "shadowCallIntercept": {
    "enabled": true,
    "model": "openai/gpt-5.6-luna"
  }
}

(State at incident time; intercept has since been disabled, after which max reasoning behaves normally.)

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    catalogModel catalog, slugs, visibility, routed entries
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 26, 2026
  2. lidge-jun commented on Aug 26, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 78 / 80

    설명

    이 이슈는 Shadow Call Intercept 대상 모델을 gpt-5.6-luna 자신으로 둔 설정에서 luna 메인 턴까지 helper 로 취급되어 reasoning.effort 가 low 로 강제되는 버그를 보고합니다. 직접 OpenAI 로는 max 로 길게 생각하는데 프록시만 얕은 답이 나옵니다. 자기 자신으로 intercept 하면 모델 rewrite 는 no-op 이고 effort clamp 만 남아 순손실입니다.

    current dev HEAD ee182a1e0 과 일치합니다. shouldInterceptShadowCall(src/lib/shadow-call.ts 56-60줄) 는 슬러그만 보고 무조건 매칭합니다(#1684 이후 helper 헤더 구분 불가). src/server/responses/core.ts 2410-2426줄은 매칭 시 모델을 _sci.model 으로 바꾸고 parsed.options.reasoning = "low" 와 raw body effort low 를 강제합니다. logCtx.requestedEffort 는 그 직전(2403줄)에 캡처되어 로그상 원래 요청과 다를 수 있습니다.

    luna 가 source 이자 target 이면 모든 luna 요청이 low 로 떨어집니다. 사용자 체감이 큰 고심각도 라우팅 버그입니다. 설정 실수에 가깝지만 제품이 자기 타겟을 허용·저장한다면 가드가 필요합니다. types.ts/config.ts 분할로 닫을 대상이 아닙니다. 미리보기 배포는 계획에 없습니다.

    src/lib/shadow-call.ts:56 - shouldInterceptShadowCall 이 슬러그 단독 매칭입니다
    src/server/responses/core.ts:2413 - intercept 시 모델 rewrite + effort low 강제입니다
    src/server/responses/core.ts:2423 - self-target 에서도 effort clamp 가 남습니다
    src/server/responses/core.ts:2403 - requestedEffort 가 clamp 전에 기록됩니다
    경로/심볼 - source 와 target 이 같으면 순수 손실이므로 설정/런타임 거부가 필요합니다

    메인테이너의 판단이 필요한 지점

    • self-target 설정을 저장 단계에서 거절할지 런타임에서 skip 할지 정해야 합니다
    • target 이 source prefix 와 겹칠 때도 막을지 정해야 합니다
    • GUI/CLI 에 위험 설정을 경고할지 정해야 합니다

    너의 추천

    열어 두세요. 최우선 버그에 가깝습니다. (1) target 모델이 source 매칭이면 intercept 를 no-op 하거나 설정을 거절하고, (2) 회귀 테스트로 luna self-target + max effort 가 low 로 안 바뀌는지 잠그세요. 문서에 luna 를 target 으로 두지 말라는 경고도 추가하세요. 미리보기 배포는 계획에 없습니다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    Fixed on dev by commit d2e9920 (PR #2849).

    Three things changed:

    • The shadow-call intercept now requires a provider+model intersection instead of matching on the model slug alone, so it no longer fires across unrelated providers.
    • The operator's configured reasoning effort is preserved. The old code forced low unconditionally, which is what silently downgraded your max-turn requests.
    • A self-target is now a no-op, and the management API rejects an invalid or self-referential shadow-call configuration with HTTP 400. The dashboard pickers no longer offer a self-target.

    Documentation was corrected alongside it — docs/shadow-call-intercept.md and the configuration reference in all eight locales still claimed replacements run at low effort.

    Verified with tests/responses-shadow-intercept.test.ts (20 pass) and the GUI picker test (6 pass), both driven RED first, plus typecheck, GUI lint, locale parity, and a 401-page docs build.

    This PR targeted dev, so GitHub did not auto-close the issue — closing manually.

  4. added a commit that references this issue on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcatalogModel catalog, slugs, visibility, routed entriesproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions