feat(providers): resolve LinkCode Gateway as a Codex-native endpoint - #477
Conversation
LinkCode Cloud now serves POST /v1/responses at the same base URL, so Codex binds natively instead of reporting an incompatible protocol.
Greptile SummaryThe PR makes LinkCode Gateway a Codex-native OpenAI Responses endpoint and adds protocol-aware model discovery and filtering.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up-review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/foundation/providers/src/catalog.ts | Adds per-variant model-list overrides and declares the Gateway’s OpenAI Responses endpoint and filtered catalog URL. |
| packages/foundation/providers/src/enabled-models.ts | Resolves each enabled account once and narrows explicitly tagged models to the agent’s selected protocol. |
| packages/foundation/schema/src/model/account.ts | Extends account models with optional protocol-availability metadata while retaining compatibility with older records. |
| packages/foundation/schema/src/wire/message.ts | Advances the wire protocol version for the additive account-model schema change. |
| packages/host/engine/src/agent/model-probe.ts | Probes distinct model-list URLs across service variants and merges model IDs with their supported protocols. |
| packages/host/engine/src/agent/request-handler.ts | Passes the complete catalog service descriptor into protocol-aware model probing. |
| packages/host/engine/src/tests/engine-model-probe.test.ts | Verifies shared-list protocol tagging and multi-list Gateway result merging. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Probe Gateway account] --> B[Fetch service-level model list]
A --> C[Fetch Responses-specific model list]
B --> D[Merge models and protocol tags]
C --> D
D --> E[Persist selected account models]
E --> F[Resolve binding for agent]
F --> G[Filter models by bound protocol]
G --> H[Offer compatible models]
Reviews (4): Last reviewed commit: "fix(schema): bump wire version for model..." | Re-trigger Greptile
…n agent can't reach Corresponds to CODE-603's client-side half: the gateway now serves a different model roster per protocol (GET /v1/models?protocol=), so the client needs to learn and act on that instead of showing every account's full catalog to every agent regardless of which wire it actually binds on. ServiceVariant gains an optional models override, so one service can serve a different list per variant while every other catalog entry (which serves the same ids on every variant) keeps using the service-level default unchanged. LinkCode Gateway's openai-responses variant now points at the protocol-filtered list. modelListSourceForProtocol resolves the effective list for one protocol; the existing modelListSource (service-level, used only as an existence check today) is untouched, so its test and the add-flow "can this be probed" gate are unaffected. AccountModel gains optional protocols: AccountProtocol[]. Absence must mean "offer it" — a pre-catalog bare key, a hand-typed custom account, or a model probed before this field existed all carry no data, and a stored account must not go dark on upgrade. enabledAccountModels narrows a picked model to the protocol the agent actually binds that account with (reusing the ResolvedBinding already computed for the enabled/unavailable check, rather than resolving twice), and treats an unknown protocol on either side — the model's or the binding's — as "offer", not "hide".
config.probe-models now resolves the EndpointService and calls the new probeServiceModels instead of a single service-level modelListSource lookup. Most services still make exactly one request (no variant overrides an otherwise-shared list), but a service like LinkCode Gateway — whose openai-responses variant lists a strict subset of the plain list — makes one request per distinct list and merges the results, tagging each returned id with every protocol whose list actually named it. A protocol the service has no variant for at all must not fall through to the service-level list by accident of `??` — fixed before it shipped, caught by a new test asserting the merge across two distinct lists rather than just the single-list case every other service exercises.
There was a problem hiding this comment.
Important
Two things to settle before merge: the wire version isn't bumped for the new AccountModel.protocols field, and the daemon's default-model fallback doesn't apply the new narrowing, so it can start codex on a model the picker just hid.
Reviewed changes — full review of all 9 files at da14af63, covering the catalog variant, the fan-out probe, and the picker narrowing.
- LinkCode Gateway gains an
openai-responsesvariant — samehttps://gateway.linkcode.ai/v1base URL, soresolveBinding(gateway, 'codex')flips fromunavailable / protocol-unsupportedtonative. - Per-variant model lists —
ServiceVariant.modelsoverrides the service-level list; the gateway's Responses variant points at/v1/models?protocol=openai-responses. NewmodelListSourceForProtocolreads override-then-default. probeServiceModelsreplacesprobeEndpointModelsas the probe entry point — dedupes distinct list URLs across a service's variants, fans out withPromise.all, merges by model id, and tags each id with every protocol whose list returned it.AccountModel.protocols— new optional field on the schema; absent means unknown, and only an explicit set narrows anything.enabledAccountModelsnarrows per binding — picked models are filtered to those reachable on the protocol the agent actually binds the account with;enabledAccountsis refactored onto a sharedresolvedAccountshelper with no behaviour change.
The backward-compatibility direction is right throughout: undefined on either side of modelReachable means "offer", so pre-catalog keys, custom accounts, and accounts probed before this field existed keep working unchanged. Both new tests genuinely fail without the change — the codex assertion would pick up anthropic/claude-sonnet-5, and the probe test pins exactly two relay paths. Scoped suite is green locally (5 files / 54 tests across providers, both engine model-probe suites, and the wire config contract).
ℹ️ A gateway deployed before the server half silently produces positively-wrong protocol tags
?protocol=openai-responses is a query parameter on the same /v1/models path the service-level list already uses. A gateway that predates linkcodehq#50 will not reject it — it will ignore it and return the full list, at which point both fan-out requests return identical data and every model gets tagged ['openai-chat', 'openai-responses']. Unlike absence, an explicit set is trusted by modelReachable, so codex would then be offered chat-only models with confidence. That degrades to exactly the CODE-620 state the PR body describes rather than to something worse, so this is a merge/deploy-ordering note rather than a code change: the client must not reach users ahead of the server.
Technical details
# Deploy ordering: client narrowing depends on a server that honours `?protocol=`
## Affected sites
- `packages/foundation/providers/src/catalog.ts:179` — the Responses variant list URL differs from the service-level one only by a query string, so an old server returns the same body for both.
- `packages/host/engine/src/agent/model-probe.ts:283` — merges protocols additively with no cross-list check that the Responses list is actually a strict subset.
- `packages/foundation/providers/src/enabled-models.ts:43-47` — `modelReachable` trusts an explicit set unconditionally.
## Required outcome
- Confirm `arcboxlabs/linkcodehq#50` is deployed to `gateway.linkcode.ai` before this ships, so the two lists genuinely differ.
## Open questions for the human
- Does the deployed gateway currently 4xx on unknown query parameters? If it does, the failure is loud instead of silent and this note is moot — but it also means the fan-out probe fails outright until the server lands (see the `Promise.all` comment).Claude Opus | 𝕏
There was a problem hiding this comment.
ℹ️ The wire bump itself is exactly right. One note on the contract test that came with it — it passes with or without the field it is meant to guard.
Reviewed changes — the da14af63→1907e145 delta, which is two things: the wire version constant and one added contract-test case.
WIRE_PROTOCOL_VERSIONbumped 78 → 79 — correct per Invariant 1, andMIN_COMPATIBLE_WIRE_VERSIONcorrectly stays at76sinceAccountModel.protocolsis purely additive. Nothing else in the repo hardcodes the version; every other site imports the constant, so the bump is self-contained.- Added a
config.get.resultcase to the wire contract suite — asserts aconfig.get.resultframe carryingmodels: [{ id, protocols }]parses.
The remaining threads from the da14af63 review are untouched by this commit and stay open — notably applyProviderDefaults at provider-config.ts:125, which is still the one blocking item.
Claude Opus | 𝕏

Client half of CODE-603. The server half is arcboxlabs/linkcodehq#50.
LinkCode Cloud now serves
POST /v1/responsesat the same base URL, so the LinkCode Gatewayservice gains an
openai-responsesvariant and Codex resolves it asnativeinstead ofreporting an incompatible protocol.
Codex ≥ 0.122 rejects
wire_api = "chat"outright, so this variant is the only thing standingbetween Codex and the Gateway; nothing else in the client hard-codes chat-only for this service,
since the settings incompatibility notice derives from
resolveBinding.Verification
see https://github.com/arcboxlabs/linkcodehq/pull/50#issuecomment-5433835876
Note
The server serves Responses for 46 of the 75 catalog models (read CODE-626 for more), while
/v1/modelsstill advertises all of them, so the model picker can offer a model that returns404 model_not_foundfor Codex.That is tracked in CODE-620 and needs a client-side decision as well as the server field.