Skip to content

feat: narrow models to what the providers/set gateway lists - #627

Merged
EugeneTheDev merged 1 commit into
agentclientprotocol:mainfrom
serjic:serjic/ijai-1783-gateway-model-discovery
Oct 14, 2026
Merged

EugeneTheDev merged 1 commit into
agentclientprotocol:mainfrom
serjic:serjic/ijai-1783-gateway-model-discovery

Conversation

@serjic

@serjic serjic commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Addresses IJAI-1783.

An organization can restrict the models its JetBrains gateway serves, but a session routed through providers/set offered Codex's whole catalogue, so the picker showed models the gateway then refused. providers/set now lists the gateway's models before it responds, and the adapter reports only the Codex models the gateway lists.

  • GET {baseUrl}/models with the headers Codex sends to the gateway (X-Client-Feature-ID: codex plus the providers/set headers); OpenAI format, data[].id. The listing is stored with the gateway config, so a provider restart reuses it, and setting the same configuration again does not list again.
  • fetchAvailableModels narrows model/list, so new, resumed, loaded and forked sessions and unstable_setSessionModel all see the same list. Ids match without case and date suffix; exactly one narrowed model stays isDefault.
  • A thread model the gateway does not list is replaced by the narrowed default, also on turn/start; a model the gateway lists but Codex's catalogue lacks is kept. Sessions open across providers/set or providers/disable take the models of the restarted app-server, and the client gets a config_option_update. A listing that matches no Codex model leaves the session with no model: there is no model config option at all.
  • A 4xx answer means the gateway has no listing: providers/set succeeds and nothing is narrowed. Any other failed listing (network error, 5xx, malformed body, 10 s timeout) fails providers/set with internal_error, and the current routing stays. A listing that finishes after a newer providers/set or providers/disable was applied is dropped (a newer request that failed drops nothing), and the routing goes to the client a provider restart installed. A baseUrl that is not an http(s) URL, accepted on v1 before, now fails with invalid_params; an invalid header or a base URL with credentials fails without repeating the value.
  • The gateway auth method and providers from the Codex config (config.toml, CODEX_CONFIG) are not listed. readme-dev.md describes the behaviour.

Not covered

Title generation (gpt-5.6-luna), subagents, /review and compaction choose their own model and are not checked against the listing.

Verification

npm run typecheck
npm run build
npm test   # 1755 passed, 36 skipped

gateway-model-discovery.test.ts covers the request and headers, narrowing and the default, the replaced thread model on turn/start, failing providers/set (HTTP error, malformed body, network error) with the routing kept, the empty list, no listing without providers/set or for the gateway auth method, providers/disable, reuse, and the provider restart. Tests that call providers/set stub the listing (stubGatewayModelListing). Checked locally against the JetBrains gateway with an earlier build of this branch: the adapter's log shows /models answering gpt-5.6-sol, gpt-6-sol, gpt-6.1-sol in one session and an empty list with only Bedrock configured; the empty-list and failing-listing behaviour of the final build is covered by the tests only.

🤖 Generated with Claude Code

Comment thread src/CodexAcpServer.ts Outdated
await this.enqueueProviderUpdate((client) => client.setProvider(params));
await this.codexAcpClient.setProvider(params);
// Copies the routing with its model listing, so the restarted app-server does not list the models again.
await this.enqueueProviderUpdate((client) => client.adoptRoutingFrom(this.codexAcpClient));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: a providers/set can be lost during a provider restart. setProvider awaits this.codexAcpClient.setProvider(...), and that call now awaits the network listing. The receiver is bound before the await. If a restart queued by an earlier providers/set swaps this.codexAcpClient while this listing is in flight, the new routing is written to the old client, which has already been replaced. This queued update then runs adoptRoutingFrom(this.codexAcpClient) and copies the previous routing from the current client.

I reproduced it with a temporary test: set A (one session, replacement initialize held), then set B with a deferred fetch, release the restart, then resolve B. Both calls succeed, but the final client reports baseUrl A. Before this PR, the queued update re-applied params on the replacement, so B won.

Suggestion: take the listing first (it does not depend on the client), then apply the config synchronously to whatever client is current, and/or keep passing the built GatewayConfig into the queued update instead of copying from this.codexAcpClient.

Comment thread src/CodexAcpClient.ts
try {
gatewayConfig.listedModels = previous?.listedModels && sameGatewayRouting(previous, gatewayConfig)
? previous.listedModels
: await listGatewayModels(gatewayConfig.config.base_url, gatewayConfig.config.http_headers);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: a providers/disable (or a newer providers/set) sent while this listing is in flight gets overwritten. previous is read before the await, and gatewayConfig is assigned unconditionally after it. So set(A) → disable → listing resolves leaves A active, even though the client's last request was the disable and both requests succeeded. I confirmed this with a temporary test: after set then disable, listProviders() still reports A's baseUrl. Two overlapping sets with different configs behave the same way: the one whose listing finishes last wins, not the one sent last.

A generation counter (or serializing the providers/* calls) would let a stale listing drop its result instead of applying it.

Comment thread src/CodexAcpClient.ts Outdated
// The gateway narrowed the models, so a model it does not list must not be used. When it lists none,
// no model is available and the thread keeps its own; the gateway then refuses the requests.
const narrowedDefault = availableModels.find(m => m.isDefault) ?? availableModels[0];
if (modelId && narrowedDefault && this.isNarrowedByGateway()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces any thread model that is not in the narrowed Codex catalogue, even when the gateway itself lists it. Say config.toml sets model = "gpt-6.1-sol", the gateway lists gpt-6.1-sol, and Codex's bundled catalogue doesn't have it yet. The session silently switches to the narrowed default. That undoes the custom-provider behaviour described in the comment just below (keep an uncatalogued configured model). Should this branch run only when the gateway does not list modelId either? For example, check listed.has(normalizeModelId(modelId)), which would need a small helper exported from GatewayModelDiscovery.

Comment thread src/ModelConfigOption.ts Outdated
if (!availableModels.some(model => model.id === currentBaseModelId)) {
// An uncatalogued current model is offered beside the catalogue. With no model at all (a gateway that
// lists none of them), nothing is offered.
if (availableModels.length > 0 && !availableModels.some(model => model.id === currentBaseModelId)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: when the gateway lists no Codex models, the model select option is returned with options: [] but currentValue is still the thread model (gpt-5.5 in the test). A select whose currentValue is not among its options is inconsistent, and clients may reject it or render it oddly. Would it be better to omit the model option entirely in this case?

@serjic
serjic force-pushed the serjic/ijai-1783-gateway-model-discovery branch 2 times, most recently from a0cecc8 to 0665800 Compare October 14, 2026 12:21
An organization can restrict the models its JetBrains gateway serves, but
the picker offered Codex's whole catalogue. When providers/set routes Codex
through a gateway, the adapter now lists the gateway's models before it
responds (GET {baseUrl}/models with the headers Codex sends) and reports
only the Codex models the gateway lists:

- ids match without case and date suffix; exactly one narrowed model stays
  the default
- a thread model the gateway does not list is replaced by the narrowed
  default, also on turn/start; one it lists but Codex does not know is kept
- sessions open across providers/set or providers/disable take the models
  of the restarted app-server, also when they are resumed later by recovery,
  and the client gets a config_option_update
- a listing that finishes after a newer providers/set or providers/disable
  was applied is dropped (a newer request that failed drops nothing), and
  the routing goes to the client a restart installed
- a 4xx answer means the gateway has no listing: providers/set succeeds and
  nothing is narrowed; any other failed listing fails providers/set with
  internal_error and keeps the current routing; a base URL that is not
  http(s) fails with invalid_params
- a listing that matches no model leaves the session none, and there is no
  model config option
- the gateway auth method and providers from the Codex config are not
  listed; the same configuration set again, and a provider restart, reuse
  the listing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@serjic
serjic force-pushed the serjic/ijai-1783-gateway-model-discovery branch from 0665800 to cd62740 Compare October 14, 2026 13:17
@EugeneTheDev
EugeneTheDev merged commit 3f555f9 into agentclientprotocol:main Oct 14, 2026
2 of 3 checks passed
Rizzen pushed a commit that referenced this pull request Oct 26, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.3.0](v2.2.2...v2.3.0)
(2026-10-26)


### Features

* add ACP v2 support again
([#620](#620))
([9e1690f](9e1690f))
* mark session list rows of worktrees Codex made (agentWorktree)
([#631](#631))
([aa02d43](aa02d43))
* narrow models to what the providers/set gateway lists
([#627](#627))
([3f555f9](3f555f9))
* report the model of each native subagent session
([#678](#678))
([51005a6](51005a6))
* return initial ACP v2 session commands
([#652](#652))
([5e9b7c5](5e9b7c5))
* support AIR sessionIndex extension
([#590](#590)) v2
([#622](#622))
([2fd758c](2fd758c))
* support append-mode ACP system prompts
([#653](#653))
([5993a12](5993a12))


### Bug Fixes

* disable Codex's Visualize skill, whose output ACP clients show as raw
text
([#667](#667))
([0ba94f5](0ba94f5))
* display the selected wire provider as JetBrains Air Gateway
([#589](#589))
([cc54abf](cc54abf))
* explain Codex config load errors caused by a deleted working directory
([#626](#626))
([a636804](a636804))
* explain invalid_encrypted_content errors after a provider change
([#668](#668))
([a77829f](a77829f))
* hold a fork back from the session list until its session/fork response
is out
([#642](#642))
([59974c7](59974c7))
* include subagent edits in the agent file-change report
([#632](#632))
([7d07123](7d07123))
* leave codex exec runs out of the session list without sessionIndex
([#639](#639))
([df75d2b](df75d2b))
* migrate to ACP SDK 2.0
([#649](#649))
([c8fd166](c8fd166))
* publish the commands when the skills change
([#643](#643))
([a62b568](a62b568))
* report structured ACP v2 failures
([#651](#651))
([5377bfe](5377bfe))
* show a Codex subagent's follow-up turn in a new subagent session
([#634](#634))
([7c761c7](7c761c7))
* synchronize ACP v2 foreground state
([#650](#650))
([986955a](986955a))
* update codex to 0.164.0
([#666](#666))
([e227c0e](e227c0e))
* wait for the compact turn to finish
([#654](#654))
([ced4c34](ced4c34))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
mrmans0n added a commit to mrmans0n/codex-acp that referenced this pull request Oct 26, 2026
Upstream v2.3.0 moved the ACP request routing from index.ts into AcpAgentRouter.ts (agentclientprotocol#620), wraps
app-server request errors with explainCodexError (agentclientprotocol#626), adds insertion-aware prompts for steering (agentclientprotocol#620),
a provider request ordering for providers/set (agentclientprotocol#627), and registers a baseline turn tracker before a
provider restart resumes a session. None of this absorbs the downstream hook review (no upstream PR).

Keep the downstream behaviour: the hook execution gate still fences thread/start, thread/resume,
thread/fork, turn/start and review/start inside the new error wrapper; session/new, load, resume,
fork and prompt still run under the review (prompt now forwards upstream's insertion); providers/set
still invalidates the review after the routing changes; a provider restart still leaves a reviewed
session for its reviewed lazy resume, before any baseline tracker is registered for it. The
codexHooks initialize capability moves into initializeExtensionsMeta and stays ACP v1 only (the
review is configured only by the v1 initialize).

_codex/hooks/trust must keep accepting reviewToken, and its route now lives in AcpAgentRouter.ts,
which is not a ledger file of this patch: this commit binds only after that path is added to the
patch files (level 3). A new router test fails without it.
Adapted from 4d43405.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants