feat(llm): add ChatGPT/Codex subscription provider - #1806
Conversation
Kodus could not run reviews billed to a ChatGPT subscription. The transport existed at libs/llm/codex-subscription-model.ts but had no product caller, no registry entry and no credential path, so LLM.run could never reach it. The transport also had a bug that only appears on multi-turn loops. reasoningEncryptedContent rides on the reasoning-start and reasoning-end parts, not reasoning-delta, so the reassembly rebuilt reasoning with no providerMetadata and the SDK filtered those items out of the next request. The include: ['reasoning.encrypted_content'] therefore bought nothing and the model re-derived its reasoning on every step of a 30-60 call review. A single-step eval call has nothing to round-trip, which is why this was not visible before. Metadata from both parts is now carried through, proven by a two-step test asserting encrypted_content appears in the second request. Also in the transport: text fragments were emitted in reverse stream order by unshift, and the response-metadata part was spread wholesale including its type field. The Proxy is replaced with an explicit LanguageModel object, which removes a receiver question under wrapLanguageModel. The provider module registers as chatgpt_subscription with capabilities measured against the live endpoint: json_schema structured output, native tool calling, no temperature support, streaming required. Models are account-scoped, so only gpt-5.6-luna and gpt-5.6-terra are listed; the other gpt-5.6 and codex ids are rejected for ChatGPT accounts. No catalog is shipped, keeping the module compatible with kodustech#1800. Credentials follow the Bedrock precedent for non-apiKey auth, stored as encrypted BYOK settings. Refresh persists the rotated refresh token before using the new access token, because the server invalidates the old one on response, and writes through a compare-and-swap on a non-request-scoped service so a worker can call it and a concurrent save cannot clobber it. A subscription has no per-token price, so usage is reported but contributes zero to spend rather than inflating it with fictional cost. Refs kodustech#1802
…on safety Review found four defects in the initial implementation. The connection test fabricated an Axios-shaped object and passed it to normalizeError, but axios.isAxiosError requires isAxiosError === true, so the object failed the check and fell through to the unknown-error tail. Every Codex failure reported code 'unknown' with the status and provider message dropped, and logged a spurious warning. An expired access token, the most likely failure for a credential that rotates every ten days, told the user nothing. The status ladder is now a separate classifyHttpStatus that both the Axios path and the Codex path call directly. The Mongo backfill still derived the canonical model as the last ':'-segment while deriveTu was changed to preserve the subscription prefix. token-usage-tu.ts asks for the two to be kept in sync, and three migrations invoke the backfill, so the next one would have re-derived every span and priced included ChatGPT usage at API rates. The aggregation now mirrors the prefix exception and the parity test covers it. The credential store was installed from the service constructor, mutating a module global in another library. That held only while Nest eagerly instantiated the provider, and would have failed as a refresh error ten days later rather than at boot. Registration moved to onModuleInit with a matching teardown. Rotation also resolved the credential by id with no organization scope, which is now passed through the slot. Finally, a persist failure during rotation discarded the new refresh token after the server had already invalidated the old one, leaving nothing usable and requiring a manual re-login. Repository errors now retry with bounded backoff and the terminal error names the recovery action. Refs kodustech#1802
The two-step reasoning test uses a mocked transport, which proves our own reassembly carries the metadata but not that the endpoint accepts the replayed encrypted_content. This spec issues real requests so the round-trip is verified against the live contract. Skips unless CODEX_LIVE=1 and a Codex credential is present, so it cannot affect CI or a contributor run. Refs kodustech#1802
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 6 suggested fixes. 🛠️ Open Agent Prompt |
What the review tools found in this branchThis branch was reviewed by Kodus and by Open Code Review before submission. Recording Kodus, reviewing itselfKodus reviewed this branch with Two of them, sharing one root cause, claimed The SQL is SELECT '{"a":1,"b":{"x":1,"y":2}}'::jsonb = '{"b":{"y":2,"x":1},"a":1}'::jsonb;
tThe third asked for path validation on Kodus's low finding count here is consistent with how it behaves generally. In a separate Open Code Review, reviewing the companion branchOCR reviewed the parallel change in alibaba/open-code-review#1106 across two passes and It was also confidently wrong three times, including one high-severity claim that Go's On the reasoning fix in this PRNeither tool found the |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
The picker listed two models, derived from a probe run against an account that was not entitled to a third. gpt-5.6-sol is served to entitled accounts and is now listed. The listing is documented as a convenience picker rather than an account inventory. The Codex backend exposes no model-list endpoint, so it cannot be dynamic, and what a ChatGPT account can run depends on its plan. The live spec now accepts a credential path and a model, so it can be driven against a credential produced by an OAuth login rather than only the Codex CLI's own file. Refs kodustech#1802
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
The live tests injected tokens straight into build(), so readCodexAuth and API_CODEX_AUTH_FILE were never exercised. This builds with no tokens passed, forcing the provider to locate and parse the credential itself. Refs kodustech#1802
What is verified here, and what is notStating this precisely, because an earlier note in this PR said "verified live" in a way Verified against the live endpointThree opt-in tests (
The first two inject a credential directly. That exercises the model and the transport. Not verifiedThe path that actually ships is untested end to end: Each piece has unit coverage, including the rotation ordering, the organization scoping The rotation path is the part I would most want exercised before this merges. It only On credential provenanceKodus has no sign-in flow, by design: it is BYOK and an administrator supplies the |
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 1 suggested fix. 🛠️ Open Agent Prompt |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
Rotation is the highest-consequence path in this provider and the one unit tests cover least convincingly, because they mock the repository. The server invalidates the old refresh token as soon as it issues a new one, so a persist that fails, writes to the wrong tenant, or loses a concurrent race costs the user their credential. Four cases against a real database: a successful swap, a swap whose expected value has different key ordering, a concurrent rotation where the loser must not clobber the winner's token, and an organization-scoped lookup that must not see another tenant's row. The key-order case is the subject of a review finding claiming JSON.stringify makes the comparison order-sensitive. The `:expected::jsonb` cast parses before comparing, so it does not, and this now demonstrates that rather than arguing it. Refs kodustech#1802
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
Correction to the test plan above, and rotation now coveredTwo updates, one of them a correction to something I stated in this PR. The reported failures were mine, not the repository'sThis PR said the only failing tests were With the correct connection settings, both pass, and so does everything else: Zero failures. Please read the earlier "8 pre-existing failures" line as withdrawn. Rotation is now exercised against a real databaseRotation was the part of this change I was least comfortable shipping on unit tests
All four pass. The key-order case is worth calling out because a review of this branch raised exactly Still not coveredThe full production chain, credential in encrypted BYOK settings through |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
Running the stack on this branchThe api and worker images were rebuilt from this branch and the running stack was Both services start clean, the worker comes up in its Two build notes for anyone reproducing this. The compose file pins A failure that is not this branch'sDriving a review through the CLI against this stack produces: Rather than assume that was unrelated, I reverted the stack to the published What is therefore still unprovenA full pull request review executing through the worker on a Codex credential remains What is verified: the provider compiles into and ships inside the production images, the |
Retracting the "control experiment" in my previous commentAn earlier comment claimed a CLI review against this stack failed to clone, that I What was actually true: Both CLI runs I based it on wrote zero bytes of output and never produced a result file, Checking it properly:
So the honest statement is that the earlier runs did not fail. I killed them. The clone I am leaving the earlier comment in place rather than editing it, so the correction is The substantive claims in this PR are unaffected: the branch images build and run, the |
Root cause of the CLI review failure, correctedMy earlier comments on this got it wrong twice. Here is what is actually happening, with There were two separate problems. A job that was never consumed. The run I watched sit at A gate that requires GitHub authentication. Once jobs were being consumed again, a
That looks worth a maintainer's attention on its own: Whether this branch caused itIt did not, and this time I checked rather than asserting:
Consequence for this PRA full review executing on a Codex credential through the worker remains unproven, and Unchanged and verified: the branch images build and run, the provider is present in the |
The review does run on this branch's imagesCorrecting my earlier account once more, this time with the authoritative record rather A CLI review executed against the worker built from this branch completed successfully. Three findings across 41 staged files, which is exactly what the same diff produced Why the job endpoint still reported PROCESSINGThe parent The severity classifier issues its own model call after the agent finishes. That call Whether this branch is responsibleThe abort surfaces through Corrections to my earlier commentsTwo things I said on this PR were wrong and are now superseded:
What stands: the branch images build, run, and execute a code review producing the |
The connection test posted `input: 'ping'` as a bare string. The Codex
endpoint requires the Responses message array form and rejects a string with
`{"detail":"Input must be a list"}`, so testing a valid ChatGPT subscription
credential always reported a 400 and told the user their model id was wrong.
Every existing test mocks fetch and asserts the URL and headers, so none of
them looked at the request body and the defect was invisible. The added test
asserts the serialised body instead.
Found by calling the endpoint through the running API rather than a mock.
Refs kodustech#1802
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
Verified through the running API, and it found a bugThe connection test path is now exercised end to end: a real POST to Doing that immediately found a defect in this PR. The bug
{"ok":false,"code":"bad_request","httpStatus":400,
"providerMessage":"{\"detail\":\"Input must be a list\"}"}So testing a perfectly valid ChatGPT subscription credential always failed, with a Worth noting why no test caught it. The three existing specs mock What the same call provesAfter the fix, the identical request returns: {"ok":true,"code":"rate_limit","httpStatus":429,
"providerMessage":"{\"error\":{\"type\":\"usage_limit_reached\",\"plan_type\":\"pro\",...}}"}Three things at once. The request shape is accepted. The credential is valid. And That last point is the review finding this PR opened with. The original code built an Method noteNone of this was reachable from the test suite. Two rounds of review, an architecture |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
…on-auth # Conflicts: # libs/llm/byok-config.ts # libs/llm/resolve-model-slot.ts # libs/organization/application/use-cases/organizationParameters/test-byok-connection.use-case.spec.ts # libs/organization/application/use-cases/organizationParameters/test-byok-connection.use-case.ts
AbortSignal.timeout rejects with a DOMException whose name the axios branches never matched, so a timed-out Codex connection test reported code 'unknown' instead of 'network'. Match by name — realm-safe — and share one timeout result builder with the axios path.
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 3 suggested fixes. 🛠️ Open Agent Prompt |
|
Review-thread dispositions (kody-ai findings): Fixed — Declined, with reasons:
|
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
…d Codex rotations Three transport fixes from deep review: - doGenerate reassembly emitted every text/reasoning fragment before all passthrough parts, reordering a stream of [text, tool-call, text] into [text, text, tool-call]. Fragments and passthrough parts are now emitted in arrival order. - A rotation whose persistence failed its full retry budget threw away the server-issued replacement while the stale refresh token was already consumed, permanently destroying the credential. The replacement is now retained in memory and the next call retries only the persistence leg; the credential store is captured at rotation start so module teardown cannot null it mid-rotation. - Text-part metadata (start/delta/end) and the finish part's providerMetadata were dropped from the synthesized doGenerate result; both now flow through like reasoning metadata.
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 1 suggested fix. 🛠️ Open Agent Prompt |
|
To continue having Ito review your PRs, upgrade your account in Ito (or ask your manager). |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
…pped The ordered-parts emission introduced for stream-order reassembly only registered fragments on start/end events. A delta arriving without a prior start wrote to the fragment map but never entered the ordered list, so its text was silently dropped at emission. Registering in the delta cases is idempotent and preserves ordering.
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 1 suggested fix. 🛠️ Open Agent Prompt |
|
@kody-ai correct on both counts — the ordered-parts reassembly in dd2664a registered fragments only on start/end events, so a delta without a prior start was dropped at emission (the pre-refactor code iterated the fragment map directly and would have kept it). Applied exactly the suggested idempotent |
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
|
@kody the "Insufficient Task Context" notice has been posted on eight review runs now, and its premise doesn't hold for the linked task. This PR fixes #1802, which has carried a full description and an explicit acceptance-criteria section since 2026-08-28 18:32Z — an hour before this PR was opened and two hours before the first notice. The endpoint contract, the bug analysis, the credential design, and the acceptance criteria are all there. If your task linkage is resolving to a Kodus-board task auto-created from the PR rather than the GitHub issue the PR closes, please use #1802 as the task context for business-rules validation. If the issue body isn't reachable to the integration, that's worth an internal look, since every run is reporting a context gap the issue does not have. |
|
@kody task context for business-rules validation, since the linked-ticket lookup keeps coming back empty. Task: #1802 — finish ChatGPT/Codex subscription auth (wire the existing transport into the product, fix the reasoning round-trip, add token refresh). Business requirements A self-hosted Kodus deployment must be able to run code reviews billed to a developer's ChatGPT subscription, with no OpenAI API key configured. Provider Business rules this change must honor
Acceptance criteria
Expected behavior after the change Admin seeds the credential through the authenticated BYOK API → reviews execute billed to the subscription → token expiry mid-review rotates without losing the credential → spend reports are unaffected. Edge cases Model availability varies by plan and over time (the listing is a convenience picker, not an account inventory; a typed-in model id still works). A review interrupted mid-rotation resumes on the persisted token. Accounts without the subscription get the endpoint's own refusal, surfaced with its provider message. |
|
@kody start-review |
Summary
Wires up the ChatGPT/Codex subscription provider that already existed in
libs/llm/codex-subscription-model.tsbut had no product caller, and fixes a bug in itstransport that only shows up on multi-turn agent loops. Fixes #1802.
Backend only. Nothing under
apps/web/or anyee/path is touched, per the scopequestion raised in the issue.
The bug worth reading about
reasoningEncryptedContentdoes not ride onreasoning-delta. In@ai-sdk/openaiit isattached to the
providerMetadataofreasoning-startandreasoning-end, whilereasoning-deltacarries onlyitemId.The existing reassembly handled
reasoning-deltaonly and rebuilt reasoning with noproviderMetadata. Understore: falsethe SDK then filters out reasoning items whoseencrypted_contentis null, emitting exactly the warning the file's own comment says itis avoiding. So
include: ['reasoning.encrypted_content']bought nothing on thedoGeneratepath, and the model re-derived its reasoning at every step of a 30-60 callreview.
The eval harness could not have caught this: a single-step call has nothing to
round-trip. The fix carries metadata from both parts through, and is proven two ways:
a mocked two-step test asserting the ciphertext appears in the second request body, and
an opt-in live test that issues real requests, since only the live endpoint proves the
replayed content is actually accepted.
Two smaller defects in the same function: text fragments were emitted in reverse stream
order by
unshift, and theresponse-metadatapart was spread wholesale including itstypefield. TheProxyis replaced with an explicitLanguageModelobject, which alsoremoves an unverifiable question about
Reflect.getwith a Proxy receiver underwrapLanguageModel.The endpoint contract, measured
streamomitted orfalse400 "Stream must be set to true"store: trueor omitted400 "Store must be set to false"temperature/max_output_tokens400 "Unsupported parameter"reasoning: {effort:"high"}+include:[reasoning.encrypted_content]200text.formatjson_schema200200, emitsfunction_call200,expires_in=864000(10 days), refresh token rotatesModels are account-scoped: which models a ChatGPT account is served depends on its plan
and changes over time.
gpt-5.6-sol,gpt-5.6-lunaandgpt-5.6-terraall return200on a Pro account;
gpt-5.6-codexandgpt-5.1-codexare refused on the accountsavailable to me. The Codex backend exposes no model-list endpoint, so the provider's
listing is a convenience picker rather than an account inventory, and a model absent
from it is still usable if typed in.
capabilities()reflectsthese measurements rather than assumptions.
Approach
Provider module at
libs/llm/providers/codex/, idchatgpt_subscription.build()reads auth lazily, because the conformance harness calls it with nocredentials. No
catalogis shipped andreasoningTraitsis provided, so the modulecompiles both on
mainand after #1800.Credentials follow the Bedrock precedent for non-
apiKeyauth: encrypted BYOKsettings, with
hasAuthand the slot field map extended so a token-only credential doesnot silently degrade to the managed default.
Refresh persists the rotated token before using the new access token, because the
server invalidates the old one the moment it responds. The write is a compare-and-swap on
a plain
@Injectable()service so a worker can call it, rather than the request-scopeduse case. A thrown repository error retries with bounded backoff instead of discarding
the newly-issued token, which would otherwise destroy the credential permanently.
Container reality.
readCodexAuthreadsos.homedir(), but the worker runs asUSER 1000innode:22-slimso that resolves to/home/node, and the installer composegives the worker no host bind mount at all. The primary path is therefore the normal
encrypted credential, which works for remote and multi-host installs.
API_CODEX_AUTH_FILEis an explicit opt-in override for the bind-mount case, never animplicit fallback.
Cost. A subscription has no per-token price, so usage is reported for observability
but contributes zero to spend rather than inflating it with fictional money. The Mongo
backfill aggregation mirrors the same prefix exception, since three migrations invoke it
and the next one would otherwise re-price every span at API rates.
Scope question
uiFieldsis not rendered anywhere; its only consumer reduces it to three booleans.Bedrock's credential form reaches the screen through hand-maintained wiring under
apps/web/src/features/ee/byok/, which is Enterprise-licensed and which #1800 alsorewrites. So this PR is backend only and credentials are seeded through the existing
admin API. Happy to follow up with the UI, or to rebase onto #1800 and land it together,
whichever you prefer.
Test plan
pnpm run test: 7103 tests pass across 735 suitesscripts/typecheck-libs-gate.shclean (0 violations)prettier --checkclean on all changed filespnpm run env:applyproduces no driftencrypted_contentreaches the second request bodyCODEX_LIVE=1) verifies both a single turn and a multi-step toolturn against the real endpoint, driven on
gpt-5.6-solwith a credential producedby a real OAuth sign-in; skips without credentials so it cannot affect CI
registry.spec.tsfive hardcoded assertions updated,CONFORMANCE_SAMPLESentryadded
build()asserted to succeed with no credentials presentrepository error
Failures unrelated to this branch:
distributed-lock.service.integration.spec.tsandcreate-or-update-config-race.integration.spec.tsboth require a live Postgres, and thisbranch touches neither.
Review notes
This branch was reviewed by both Kodus and Open Code Review before submission; findings
from each are summarised in a comment below.