Add on-device Android port (lwc-core, lwc-android, Compose sample) - #5
Open
RohitWaghire wants to merge 3 commits into
Open
Add on-device Android port (lwc-core, lwc-android, Compose sample)#5RohitWaghire wants to merge 3 commits into
RohitWaghire wants to merge 3 commits into
Conversation
Kotlin port of the SDK that runs entirely on-device — no server, no API key. Users sign in with their own ChatGPT subscription via the device-code flow and stream Codex models billed to their plan. - lwc-core: pure Kotlin/JVM engine mirroring packages/core (device OAuth, token refresh, JWT parsing, SSE streaming to Flow<String>); 14 JUnit tests ported from the TS test oracles. - lwc-android: Keystore-backed encrypted token store, Custom Tab launcher, LoginWithChatGPT facade. - sample: minimal Compose app (login -> device code -> streamed chat). Verified end-to-end against a real ChatGPT account: device login, model discovery, and a streamed gpt-5.5 completion (Stage-0 spike, `gradlew :lwc-core:run`). Wire-format requirements discovered live and encoded in the transport: /responses needs `stream: true`, and `input` must be a list of message items.
|
@RohitWaghire is attempting to deploy a commit to the OpenCore Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds a standalone Android port for Login with ChatGPT. The main changes are:
Confidence Score: 5/5Safe to merge with low risk from the reviewed changes. No blocking or non-blocking issues were found in the reviewed Android modules, core OAuth and transport logic, sample app, or tests. The earlier backup, Custom Tab launch, and custom-port concerns are addressed in the current diff. No files require special attention.
What T-Rex did
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant App as Compose sample / app
participant Facade as LoginWithChatGPT
participant Core as lwc-core OAuth + Codex
participant Store as KeystoreTokenStore
participant Browser as Custom Tab
participant OpenAI as OpenAI / ChatGPT endpoints
App->>Facade: startDeviceLogin()
Facade->>Core: requestDeviceCode(config)
Core->>OpenAI: POST /deviceauth/usercode
OpenAI-->>Core: device_auth_id + user_code
Core-->>Facade: DeviceCode
Facade-->>App: DeviceCode
App->>Facade: openVerification(device)
Facade->>Browser: launch verification URL
Browser->>OpenAI: user enters code
loop until authorized or expired
App->>Facade: poll(device)
Facade->>Core: pollDeviceCode(config, device)
Core->>OpenAI: POST /deviceauth/token
OpenAI-->>Core: pending or authorization_code + PKCE
end
Facade->>Core: exchangeDeviceAuthorization(...)
Core->>OpenAI: POST /oauth/token
OpenAI-->>Core: access / refresh / id tokens
Facade->>Store: save(tokens)
App->>Facade: chat(body)
Facade->>Store: load()
Facade->>Core: ensureFreshTokens + codexResponses
Core->>OpenAI: "POST /backend-api/codex/responses stream=true"
OpenAI-->>Core: SSE text deltas
Core-->>App: "Flow<String> deltas"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant App as Compose sample / app
participant Facade as LoginWithChatGPT
participant Core as lwc-core OAuth + Codex
participant Store as KeystoreTokenStore
participant Browser as Custom Tab
participant OpenAI as OpenAI / ChatGPT endpoints
App->>Facade: startDeviceLogin()
Facade->>Core: requestDeviceCode(config)
Core->>OpenAI: POST /deviceauth/usercode
OpenAI-->>Core: device_auth_id + user_code
Core-->>Facade: DeviceCode
Facade-->>App: DeviceCode
App->>Facade: openVerification(device)
Facade->>Browser: launch verification URL
Browser->>OpenAI: user enters code
loop until authorized or expired
App->>Facade: poll(device)
Facade->>Core: pollDeviceCode(config, device)
Core->>OpenAI: POST /deviceauth/token
OpenAI-->>Core: pending or authorization_code + PKCE
end
Facade->>Core: exchangeDeviceAuthorization(...)
Core->>OpenAI: POST /oauth/token
OpenAI-->>Core: access / refresh / id tokens
Facade->>Store: save(tokens)
App->>Facade: chat(body)
Facade->>Store: load()
Facade->>Core: ensureFreshTokens + codexResponses
Core->>OpenAI: "POST /backend-api/codex/responses stream=true"
OpenAI-->>Core: SSE text deltas
Core-->>App: "Flow<String> deltas"
Reviews (3): Last reviewed commit: "Handle undecryptable token store after b..." | Re-trigger Greptile |
…etUrl Review findings from the PR: - VerificationLauncher now sets FLAG_ACTIVITY_NEW_TASK — the facade hands it an application context, and starting an activity from a non-Activity context without the flag throws AndroidRuntimeException. - resolveTargetUrl rebuilds the URL via HttpUrl.newBuilder() so a custom port in an overridden codexBaseUrl (e.g. http://10.0.2.2:8080) survives, matching the TS implementation's use of `origin`. Adds a regression test.
Auto Backup can restore the encrypted prefs file onto a device whose Keystore lacks the original master key, making it undecryptable and crashing EncryptedSharedPreferences at startup. - sample: disable allowBackup — the token store can't survive a restore by design, since the master key never leaves the device. - KeystoreTokenStore: self-heal instead of crash — if the file can't be decrypted, wipe it and recreate, so the user just re-authenticates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This add an Android as a first-class target: a Kotlin port of the SDK that runs
entirely on-device. Users sign in with their own ChatGPT subscription via the
device-code flow, tokens live in Android-Keystore-encrypted storage, and apps
stream Codex models billed to the user's plan - same model as the web SDK, but
with no server to host (the natural trust boundary for a native app, like the
Codex CLI itself).
No changes to the existing TS packages - everything lives in a new
android/Gradle project.Modules
lwc-core- pure Kotlin/JVM engine miroringpackages/core: device-codeOAuth, token exchange/refresh, JWT claims, and
/responsesSSE streaming asa
Flow<String>. Tests ported 1:1 frompackages/core/test.lwc-android- Keystore-backedTokenStore, Custom Tab launcher,LoginWithChatGPTfacade.sample- minimal Compose app: sign-in → device code → streamed chat.Verification
./gradlew :lwc-core:test- 14/14 pass (JUnit ports of the TS oracles)../gradlew :lwc-core:run) passed against a realChatGPT account: device login → model discovery → streamed gpt-5.5 reply.
./gradlew :sample:assembleDebugproduces a working APK.Notes for review
CodexTransport.kt:/responsesneedsstream: truefor SSE, andinputmust be a list of message items (a bare string gets
400 {"detail":"Input must be a list"}). The TS core may want the sameguard in
normalizeResponsesBodysomeday.packages/core/src/codex-transport.ts; divergences are commented.Happy to adjust structure/naming to fit the repo's conventions.