Skip to content

Add on-device Android port (lwc-core, lwc-android, Compose sample) - #5

Open
RohitWaghire wants to merge 3 commits into
opencoredev:mainfrom
RohitWaghire:android-port-upstream
Open

Add on-device Android port (lwc-core, lwc-android, Compose sample)#5
RohitWaghire wants to merge 3 commits into
opencoredev:mainfrom
RohitWaghire:android-port-upstream

Conversation

@RohitWaghire

Copy link
Copy Markdown

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 miroring packages/core: device-code
    OAuth, token exchange/refresh, JWT claims, and /responses SSE streaming as
    a Flow<String>. Tests ported 1:1 from packages/core/test.
  • lwc-android - Keystore-backed TokenStore, Custom Tab launcher,
    LoginWithChatGPT facade.
  • sample - minimal Compose app: sign-in → device code → streamed chat.

Verification

  • ./gradlew :lwc-core:test - 14/14 pass (JUnit ports of the TS oracles).
  • Live end-to-end spike (./gradlew :lwc-core:run) passed against a real
    ChatGPT account: device login → model discovery → streamed gpt-5.5 reply.
  • ./gradlew :sample:assembleDebug produces a working APK.

Notes for review

  • Two wire-format requirements surfaced by the live run are encoded in
    CodexTransport.kt: /responses needs stream: true for SSE, and input
    must be a list of message items (a bare string gets
    400 {"detail":"Input must be a list"}). The TS core may want the same
    guard in normalizeResponsesBody someday.
  • Kotlin transport behavior intentionally mirrors
    packages/core/src/codex-transport.ts; divergences are commented.

Happy to adjust structure/naming to fit the repo's conventions.

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.
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@RohitWaghire is attempting to deploy a commit to the OpenCore Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a standalone Android port for Login with ChatGPT. The main changes are:

  • New Android Gradle project with lwc-core, lwc-android, and sample modules.
  • Kotlin/JVM core for device-code OAuth, token refresh, JWT parsing, Codex model listing, and SSE response streaming.
  • Android facade with Keystore-backed encrypted token storage and Custom Tab verification launch.
  • Compose sample app for sign-in, device-code polling, logout, and streamed chat.
  • JVM tests covering Codex body normalization, URL mapping, client version handling, model parsing, and JWT helpers.

Confidence Score: 5/5

Safe 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.

T-Rex T-Rex Logs

What T-Rex did

  • The proof transcript was saved for review.
  • The transcript shows java not found, javac not found, an empty JAVA_HOME, and a Gradle failure indicating JAVA_HOME is not set and no java command could be found in the PATH.
  • Because the test task could not start in the sandbox, there is no evidence of a PR code failure.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
android/lwc-core/src/main/kotlin/com/opencoredev/loginwithchatgpt/CodexTransport.kt Adds Codex request normalization, URL mapping, SSE streaming, and model-list extraction; no issues found.
android/lwc-core/src/main/kotlin/com/opencoredev/loginwithchatgpt/Device.kt Adds device-code request, polling, token exchange, and wait-loop support; no issues found.
android/lwc-core/src/main/kotlin/com/opencoredev/loginwithchatgpt/OAuth.kt Adds authorization-code exchange and refresh-token handling with dead-refresh error mapping; no issues found.
android/lwc-android/src/main/kotlin/com/opencoredev/loginwithchatgpt/android/KeystoreTokenStore.kt Adds Keystore-backed encrypted token persistence with recovery from undecryptable restored preferences; no issues found.
android/lwc-android/src/main/kotlin/com/opencoredev/loginwithchatgpt/android/LoginWithChatGPT.kt Adds the Android facade wiring device login, token storage, refresh, model listing, and streaming chat; no issues found.
android/lwc-android/src/main/kotlin/com/opencoredev/loginwithchatgpt/android/VerificationLauncher.kt Adds Custom Tab launch support with the required new-task flag for application contexts; no issues found.
android/sample/src/main/AndroidManifest.xml Adds internet permission and launcher activity with backups disabled for encrypted token safety; no issues found.
android/sample/src/main/kotlin/com/opencoredev/loginwithchatgpt/sample/MainActivity.kt Adds a Compose sample covering login, polling, logout, and streamed chat; no issues found.
android/lwc-core/src/test/kotlin/com/opencoredev/loginwithchatgpt/CodexTransportTest.kt Adds Kotlin tests for Codex body normalization, URL mapping, client versioning, and model parsing; no issues found.
android/lwc-core/src/test/kotlin/com/opencoredev/loginwithchatgpt/JwtTest.kt Adds JWT decoding, account-id derivation, expiry, and user-profile tests; no issues found.

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"
Loading
%%{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"
Loading

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.
Comment thread android/sample/src/main/AndroidManifest.xml
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.
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.

1 participant