Skip to content

Add ChatGPT subscription LLM support#744

Merged
neubig merged 22 commits into
mainfrom
add-llm-subscription-support
Jun 12, 2026
Merged

Add ChatGPT subscription LLM support#744
neubig merged 22 commits into
mainfrom
add-llm-subscription-support

Conversation

@neubig

@neubig neubig commented May 23, 2026

Copy link
Copy Markdown
Member

@/tmp/pr744-body-updated.txt


🐳 Docker images for this PR

GHCR package: https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas

Component Value
Image ghcr.io/openhands/agent-canvas
Architectures amd64, arm64
Agent Server ghcr.io/openhands/agent-server:1.28.1-python
Automation openhands-automation==1.0.0a9
Commit f96869da032d256ee6d3dfaffadc6cab69556be4

Pull (multi-arch manifest)

# Multi-arch manifest — Docker automatically pulls the correct architecture
docker pull ghcr.io/openhands/agent-canvas:sha-f96869d

Run

docker run -it --rm \
  -p 8000:8000 \
  ghcr.io/openhands/agent-canvas:sha-f96869d

All tags pushed for this build

ghcr.io/openhands/agent-canvas:sha-f96869d-amd64
ghcr.io/openhands/agent-canvas:add-llm-subscription-support-amd64
ghcr.io/openhands/agent-canvas:pr-744-amd64
ghcr.io/openhands/agent-canvas:sha-f96869d-arm64
ghcr.io/openhands/agent-canvas:add-llm-subscription-support-arm64
ghcr.io/openhands/agent-canvas:pr-744-arm64
ghcr.io/openhands/agent-canvas:sha-f96869d
ghcr.io/openhands/agent-canvas:add-llm-subscription-support
ghcr.io/openhands/agent-canvas:pr-744

About Multi-Architecture Support

  • Each tag (e.g., sha-f96869d) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., sha-f96869d-amd64) are also available if needed

Co-authored-by: openhands <openhands@all-hands.dev>
@vercel

vercel Bot commented May 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-canvas Ready Ready Preview, Comment Jun 12, 2026 9:59pm

Request Review

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary

🟡 Acceptable with critical issues - The subscription auth feature is well-structured overall, but has fundamental issues that must be addressed before merging.

[CRITICAL ISSUES]

  • [__tests__/api/llm-subscription-service.test.ts] Test Quality: The core test suite violates repository guidelines by mocking the entire LLMMetadataClient HTTP client instead of testing real code paths with MSW. This means tests won't catch actual regressions when the HTTP layer, auth headers, or server contract changes. Rewrite to use the MSW handlers already available in settings-handlers.ts.

  • [src/components/features/settings/llm-profiles/llm-settings-local-view.tsx:170] Complexity: The handleSave function has >4 levels of nesting (try-catch → rename if → auth type if-else → subscription model if → API key if-else → base_url if → wasActive if), violating the repository's '3 levels max' guideline. Extract helpers: buildLlmConfigForAuthType(), handleProfileRename(), handleProfileReactivation().

  • [src/api/llm-subscription-service.ts:78] Type Safety: Unsafe casting (client as unknown as LLMMetadataClientWithTransport).client to access internal transport. If @openhands/typescript-client changes its internal structure, this fails at runtime with cryptic errors. Add runtime validation that the transport exists and has the expected methods before using it.

[IMPORTANT ISSUES]

  • [src/components/features/settings/llm-settings/openai-subscription-auth-card.tsx:61] User Experience: When polling returns connected: false, an error toast is shown with a "PENDING" message. This is misleading—users think something failed when they just need to wait. Use an info/warning toast or inline status text instead.

  • [src/components/features/settings/llm-settings/openai-subscription-auth-card.tsx:36] State Management: Users can get stuck in the challenge state if they start device login but close the tab or navigate away. No way to clear the challenge except completing login or logging out (which fails if not connected). Add a "Cancel" button to reset challenge state.

  • [src/routes/llm-settings.tsx:219] Data Flow: Model selection is silently changed when switching auth types. If a user has 'gpt-4o', switches to subscription to explore, then switches back, their original selection is lost forever (replaced with defaultModel). Store previous models in component state and restore them, or at minimum show a toast explaining the change.

  • [src/api/llm-subscription-service.ts:109] Validation: Missing validation for empty strings in normalizeDeviceChallenge. A buggy server could return empty strings that pass the !deviceCode check. Add .trim().length > 0 validation for deviceCode, userCode, and verificationUri.

  • [__tests__/api/llm-subscription-service.test.ts] Test Coverage: Missing critical test cases: error handling (401/403, 500, timeouts, malformed responses), validation (incomplete device challenges), end-to-end integration (full device flow), and edge cases (empty strings, null values, concurrent requests).

  • [src/api/llm-subscription-service.ts:87] Error Handling: Network errors from the HTTP client propagate without context. Wrap errors to provide more specific messages about which subscription operation failed.

[SUGGESTIONS]

  • [src/api/agent-server-adapter.ts:820] Documentation: Add docstring to assertSubscriptionAuthReady noting that it's NOT called when resuming conversations or sending additional messages, so subscriptions could expire mid-conversation. The agent-server must handle expired tokens gracefully.

  • [src/api/llm-subscription-service.ts:136] Data Structure: The expiresAt field tries both string and number fallbacks, including expires_in which is typically a relative duration in seconds, not an absolute timestamp. Mixing relative and absolute time values in the same field could cause confusion. Document the expected behavior or convert relative durations to absolute timestamps.

  • [src/hooks/query/use-llm-subscription-status.ts:14] Query Configuration: staleTime of 30 seconds seems short for auth status that rarely changes. Consider 5 minutes to reduce unnecessary network requests, especially since mutations properly invalidate this query.

[RISK ASSESSMENT]

🟡 MEDIUM RISK - This PR adds new authentication flows and modifies conversation creation logic. The main risks are:

  • Untested error paths could cause poor UX when auth fails
  • Complex nested logic increases maintenance burden
  • Type safety issues could cause runtime failures with SDK updates

Not blocking auto-merge based on risk alone, but the critical issues above should be addressed.

VERDICT:

Needs rework - Address the critical test quality and complexity issues, then fix the important UX and validation gaps.

KEY INSIGHT:

The subscription auth implementation follows OAuth device flow patterns correctly, and backward compatibility is preserved (API key flows still work). However, the test suite doesn't provide confidence that the integration will work in production, and the complex nested save logic creates maintenance risk. Focus first on rewriting tests to use MSW and refactoring handleSave.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/agent-canvas/actions/runs/26336381445

Co-authored-by: openhands <openhands@all-hands.dev>
- Merge /api/llm/subscription/openai/models with chatgpt/ provider
  models from /api/llm/models so the subscription dropdown stays in
  sync with the full LiteLLM registry (e.g. includes gpt-5.5)
- Add gpt-5.3-codex to the hardcoded fallback constant
- Replace hardcoded OPENAI_SUBSCRIPTION_MODELS array in the dropdown
  with a dynamic useOpenAISubscriptionModels hook (falls back to
  constant when endpoints are unavailable)
- Device code block: remove 'Code: ' prefix, fix black-on-dark
  contrast by using --oh-surface-primary / --oh-text-primary tokens,
  add CopyToClipboardButton for one-click copy

Co-authored-by: openhands <openhands@all-hands.dev>
LiteLLM's chatgpt/ provider registry does not yet include gpt-5.5,
but it is available via ChatGPT Plus/Pro subscription. Add it to the
hardcoded fallback so it appears in the dropdown regardless of whether
the backend's chatgpt/ entries have been updated.

Co-authored-by: openhands <openhands@all-hands.dev>
- Drop /api/llm/subscription/openai/models as a model source; it was
  added in this PR set and is redundant — LiteLLM's chatgpt/ provider
  is the single authoritative list
- Remove the OPENAI_SUBSCRIPTION_MODELS hardcoded fallback array,
  OpenAISubscriptionModel type, DEFAULT_OPENAI_SUBSCRIPTION_MODEL, and
  isOpenAISubscriptionModel type guard — all replaced by the live
  subscriptionModels from the hook, or a simple pass-through in
  non-hook contexts (adapter, profile builder)
- Update agent-server-adapter test to reflect pass-through behavior

Co-authored-by: openhands <openhands@all-hands.dev>
@rajshah4

Copy link
Copy Markdown
Member

I tested the ChatGPT subscription flow locally. The frontend/model-selection parts of this PR helped, but the remaining
conversation-start failure appears to be in software-agent-sdk / Agent Server runtime handling, not the UI.

Observed behavior:

  • /api/llm/subscription/openai/status reports connected.
  • /api/llm/subscription/openai/models returns models.
  • Direct calls to the ChatGPT Codex endpoint with the stored OAuth token work.
  • Direct LiteLLM responses and aresponses calls also work with:
    • api_base=https://chatgpt.com/backend-api/codex
    • model=openai/gpt-5.3-codex
    • streaming enabled
    • Codex headers
  • But starting a normal Agent Canvas conversation fails with:
litellm.AuthenticationError: AuthenticationError: OpenAIException - {"detail":"Could not parse your authentication
token. Please try signing in again."}

The likely root cause is that the SDK loses subscription runtime state after serializing/reloading the LLM config.

A saved conversation LLM contains:

{
"auth_type": "subscription",
"subscription_vendor": "openai",
"model": "openai/gpt-5.3-codex"
}

But after validating/reloading it as an SDK LLM, llm.is_subscription becomes False. That means
_get_litellm_api_key_value() does not fetch the ChatGPT OAuth access token, so the ChatGPT Codex endpoint receives invalid/missing auth and returns the token-parse error.

Potential SDK fix:

@Property
def is_subscription(self) -> bool:
return self._is_subscription or (
self.auth_type == "subscription"
and self.subscription_vendor == "openai"
)

There is a second issue after auth is fixed: LiteLLM’s streamed Responses completion can expose completed_event.response as a plain dict, while the SDK expects an object with .output. I verified the stream output can be recovered from response.output_item.done events and parsed successfully.

So I think this PR may need a companion SDK fix for:

  1. Preserving/rederiving subscription mode after LLM config serialization.
  2. Handling dict-shaped streamed Responses completion objects.
  3. Optionally filtering subscription models to ones actually usable by the account.

Co-authored-by: openhands <openhands@all-hands.dev>

neubig commented May 28, 2026

Copy link
Copy Markdown
Member Author

Updated this stacked PR in 1e94188: merged current main, resolved package conflicts, kept the temporary @openhands/typescript-client branch pin at b025805, rewrote subscription service tests to use MSW/real client paths, tightened device challenge validation, added login cancel/pending UX, preserved model selection when toggling auth type, and refactored profile-save logic. Local verification: npm run lint, npm run build, and full npm test passed.\n\nThis comment was generated by an AI agent (OpenHands) on behalf of neubig.

@openhands-ai

openhands-ai Bot commented May 28, 2026

Copy link
Copy Markdown

Uh oh! There was an unexpected error starting the job :(

all-hands-bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary

Previous critical issues resolved — The three blockers from the prior review have all been addressed: tests now use MSW handlers instead of full-client mocks, handleSave complexity has been properly refactored into helper functions (buildProfileLlmConfig, renameProfileIfNeeded, reactivateProfileIfNeeded), and the unsafe (client as unknown as LLMMetadataClientWithTransport).client cast is gone. The cancel button, model restoration refs, and empty-string validation in readString are all present. This is a significant quality improvement.

The OAuth device flow pattern is correctly implemented, subscription credential omission from conversation payloads is good security practice, and withLlmClient's finally { client.close() } is solid resource management.


[IMPORTANT ISSUES]

  • Unconditional subscription model fetch (src/routes/llm-settings.tsx:140): useOpenAISubscriptionModels() fires on every mount of LlmSettingsScreen, including for users who never use subscription auth. The hook supports { enabled } — pass enabled: isSubscriptionMode where isSubscriptionMode is derived from the current settings' auth type. The silent failure path means no visible bug, but it generates unnecessary traffic for all API-key users and may produce 404/500 noise in agent-server logs.

  • Race condition: model saved as empty string (src/routes/llm-settings.tsx:408): In buildPayload, if subscriptionModels is still undefined when the user saves (e.g. they switch to subscription and immediately click save before the model list loads), subscriptionModels?.[0] ?? "" evaluates to "". The same race exists in handleAuthTypeChange at line 235 — switching before models load fires onChange("llm.model", ""). The subscriptionModelValue display fallback at line 181–183 hides this in the UI, but the underlying form value is "". Guard the save path: if authType === LLM_AUTH_TYPE_SUBSCRIPTION && !subscriptionModels?.length, show a validation message or block the save.

[SUGGESTIONS]

  • assertSubscriptionAuthReady missing lifecycle note (src/api/agent-server-adapter.ts:844): Flagged in the previous review, still unaddressed. The function is called at conversation start and profile switch, but not on subsequent message sends or conversation resume. A one-line JSDoc noting this scope prevents future maintainers from assuming token expiry is covered end-to-end.

  • @openhands/typescript-client git dep tracking (__tests__/package-library.test.ts:57): The comment says "temporarily allowed while this stacked PR waits for the subscription client branch to merge/release" but there is no linked issue or deadline. Link a tracking issue or PR in the comment so this exemption doesn't outlive its purpose.

  • staleTime of 30s for subscription status (src/hooks/query/use-llm-subscription-status.ts:14): Raised in the previous review. Since both poll and logout mutations already invalidate this query, the 30-second background refetch is redundant. Bumping to 5 minutes reduces background requests with no UX regression.


VERDICT: 🟡 Close to merge-ready. The race condition in buildPayload/handleAuthTypeChange can silently produce a profile with an empty model field — worth a targeted fix before merge. The unconditional model fetch is a UX polish issue. Everything else is non-blocking.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread src/routes/llm-settings.tsx Outdated
Comment thread src/routes/llm-settings.tsx Outdated
Comment thread src/routes/llm-settings.tsx Outdated
Comment thread src/api/agent-server-adapter.ts
Comment thread __tests__/package-library.test.ts
Comment thread src/hooks/query/use-llm-subscription-status.ts Outdated
Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai

openhands-ai Bot commented May 28, 2026

Copy link
Copy Markdown

@openhands-ai[bot] it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again.

1 similar comment
@openhands-ai

openhands-ai Bot commented May 28, 2026

Copy link
Copy Markdown

@openhands-ai[bot] it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again.

Co-authored-by: openhands <openhands@all-hands.dev>

all-hands-bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review: Add ChatGPT subscription LLM support

🟡 Acceptable - The implementation is functional and the architecture is reasonable, but there are a few issues that should be addressed before merging.


[CRITICAL ISSUES]

  • [src/routes/llm-settings.tsx, ~line 450] Error thrown in callback: buildPayload throws new Error("Subscription models are not loaded yet.") when subscription models haven't loaded. This is problematic because:

    • Raw Error thrown in a React callback won't be caught gracefully
    • Poor user experience - users will see a cryptic error instead of a loading state
    • Consider returning a validation result object instead, or preventing form submission until models are loaded
  • [src/mocks/workspace-handlers.ts] Unrelated file: This new file contains workspace mock handlers and appears completely unrelated to LLM subscription support. It should be removed from this PR or moved to a separate PR.


[IMPROVEMENT OPPORTUNITIES]

  • [src/routes/llm-settings.tsx, handleAuthTypeChange] Complex branching: This function has deep nesting with multiple early returns for auth type switching. Consider extracting the model persistence/switching logic into a separate helper function to improve readability.

  • [src/components/features/settings/llm-profiles/llm-settings-local-view.tsx] Duplicate auth handling: The auth type logic in handleSave duplicates code from llm-settings.tsx. Consider extracting this into a shared utility to reduce code duplication.

  • [src/api/agent-server-adapter.ts, assertSubscriptionAuthReady] Vendor-specific check: This function is hardcoded to check OpenAI subscription status, but the schema suggests future subscription vendors. Consider renaming or adding a vendor parameter to make this extensible.


[TESTING GAPS]

  • [tests/components/settings/llm-profiles/llm-settings-local-view.test.tsx] Test regression: The test "calls save mutation with correct payload and returns to list" was simplified from a full integration test with proper userEvent interactions to a simpler test with fireEvent. The new test doesn't verify the actual save behavior - it just checks that the create view remains stable. Consider restoring the original test behavior or renaming the test to reflect what it actually tests.

[STYLE NOTES]

  • The new src/mocks/workspace-handlers.ts file (89 lines) is completely unrelated to this PR. Please remove it or explain why it belongs here.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    The core functionality (ChatGPT subscription auth via device code flow) is implemented correctly. However, the error-throwing behavior in buildPayload poses a runtime risk that could cause unhandled exceptions. The unrelated workspace-handlers.ts file adds noise and potential confusion.

VERDICT:

Needs rework: The error-throwing pattern in buildPayload must be fixed before merging. The unrelated workspace-handlers.ts file should be removed.

KEY INSIGHT:

Throwing raw Error objects inside React callbacks is an anti-pattern that leads to poor user experience; use validation result objects or conditional rendering instead.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@github-actions

Copy link
Copy Markdown
Contributor

✅ Mock-LLM E2E Tests

53/53 passed

Commit: 9a20a117 · Workflow run · Test artifacts

Status Test Duration
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.7s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.6s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.3s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.8s
mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.2s
mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.3s
mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.2s
mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.4s
mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.8s
mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 748ms
mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.1s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 29.4s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.1s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.3s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 6.4s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 7.3s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.7s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 15.8s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 20.7s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 197ms
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.8s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.6s
mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.7s
mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 13.4s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.5s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 2: clicking GitHub card opens the install modal with correct fields 10.4s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 3: full install flow — fill PAT, submit, verify installed 12.7s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 4: installed GitHub server can be deleted 5.8s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 1: configure LLM, create switch-target profile, register trajectory 13.1s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch 6.8s
mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation 4.6s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › keeps the modal open on backdrop click and Escape 1.5s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to OpenAI GPT-5.5 1.7s
mock-llm-partial-stack.spec.ts › partial stack: --frontend-only › serves the frontend but returns 503 for backend routes 7.3s
mock-llm-partial-stack.spec.ts › partial stack: --backend-only › serves backend APIs but returns 503 for the frontend root 13.1s
mock-llm-partial-stack.spec.ts › partial stack: port conflict › fails with a clear error when the ingress port is occupied 107ms
mock-llm-partial-stack.spec.ts › partial stack: port conflict › starts successfully on a free port after a conflict 6.0s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › automation card sends the correct slash command to a conversation 16.5s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › direct slash command from home page triggers skill activation 13.4s
mock-llm-profile-management.spec.ts › active profile deletion + reconciliation › active profile is deletable and reconciliation activates another profile 8.5s
mock-llm-profile-management.spec.ts › same-model profile identity › chat header shows the correct profile when two profiles share the same model 15.0s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › project skill in workspace/.agents/skills/ triggers on matching keyword 13.6s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › user skill in ~/.openhands/skills/ triggers on matching keyword 13.4s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › deleting a user skill removes it from subsequent conversations 13.4s
mock-llm-ui-regressions.spec.ts › UI regressions › scopes standalone styles to the agent-server-ui shell 930ms
mock-llm-ui-regressions.spec.ts › UI regressions › renders critic results on agent messages and finish actions 1.4s
mock-llm-ui-regressions.spec.ts › UI regressions › loads older events when scrolling up 1.5s
mock-llm-ui-regressions.spec.ts › UI regressions › selected workspace persists after navigating away and returning 2.0s
mock-llm-ui-regressions.spec.ts › UI regressions › cleared sessionStorage yields empty workspace selection 918ms

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@github-actions

Copy link
Copy Markdown
Contributor

🛑 Mock-LLM Docker E2E Test Results

27/29 passed · 2 skipped · ⚠️ 24 not run (process killed at 29/53)

Commit: 9a20a117 · Workflow run · Test artifacts

Status Test Duration
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.8s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.5s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.7s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.7s
chromium › mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.7s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.2s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.8s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 774ms
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.2s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 32.5s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.2s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.3s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 7.7s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 6.3s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.8s
⏭️ chromium › mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 184ms
⏭️ chromium › mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 185ms
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 195ms
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.4s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.3s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.2s
chromium › mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.4s
chromium › mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 13.3s
chromium › mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.5s

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@neubig

neubig commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

Addressed the fresh AI review in 909082f6:

  • disabled subscription auth/model controls while the subscription model list is still loading
  • stopped the subscription model dropdown from writing llm.model = "" when no model is available
  • added a regression test for the loading state
  • added the requested subscription-auth lifecycle docstring
  • bumped subscription status staleTime to 5 minutes

I did not remove src/mocks/workspace-handlers.ts: that file already exists on main (65e17127 Add workspace mock handlers for snapshots) and is not introduced by this PR.

Local validation:

  • npm run typecheck
  • npx vitest run __tests__/routes/llm-settings.test.tsx __tests__/api/agent-server-adapter.test.ts __tests__/hooks/query/use-agent-settings-schema.test.tsx __tests__/api/llm-subscription-service.test.ts

all-hands-bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review: Add ChatGPT subscription LLM support

Taste Rating

🟡 Acceptable — Clean implementation with minor UX and robustness gaps worth addressing before merge.


Analysis

This PR introduces support for ChatGPT subscription-based LLM authentication via OpenAI's device code flow. The implementation follows the existing patterns in the codebase and adds:

  • A subscription service for device authentication
  • React Query hooks for status and model fetching
  • UI components for the auth card and subscription settings
  • Schema injection for subscription-related fields

[IMPROVEMENT OPPORTUNITIES]

  1. 🟡 Manual polling UXopenai-subscription-auth-card.tsx:64-78 requires users to click "Finish Sign In" after completing browser authentication. Consider auto-polling with a retry loop to match standard device code flow patterns.

  2. 🟡 No retry/refresh on subscription unavailableopenai-subscription-auth-card.tsx:118-121 shows "unavailable" but provides no way to retry. Consider adding a retry button or refetch on click.

  3. 🟡 Subscription models cached indefinitelyuse-llm-subscription-models.ts:14 uses staleTime: 1000 * 60 * 5 but models could change (user upgrades/downgrades). Consider shorter staleTime or adding a refetch mechanism when auth status changes.

  4. 🟡 No cleanup for expired device challenges — If the user starts login but doesn't complete it within the challenge expiry window, the UI doesn't show this state. The expiresAt field is stored but never displayed.


[TESTING]

✅ Tests in __tests__/api/llm-subscription-service.test.ts cover the main service methods including error cases and normalization logic.

✅ MSW handlers in src/mocks/settings-handlers.ts properly mock the subscription endpoints.


[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

This is an additive feature with no breaking changes to existing functionality. The subscription auth is opt-in via a new auth type selector. Code paths are guarded by the isSubscriptionLlmConfig check, so existing API key users are unaffected. No new dependencies are introduced.


Verdict

Worth merging — Core logic is sound, with suggestions for improving UX and robustness.

Key Insight

The device code flow implementation is correct per OAuth standards, but requires manual user action to poll — consider auto-polling to match expected behavior for this auth pattern.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing. See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@github-actions

Copy link
Copy Markdown
Contributor

✅ Mock-LLM E2E Tests

53/53 passed

Commit: 909082f6 · Workflow run · Test artifacts

Status Test Duration
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.9s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.6s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.7s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.8s
mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.4s
mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.3s
mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.2s
mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.4s
mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.4s
mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 763ms
mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 9.4s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 27.4s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.1s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.3s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 6.2s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 6.5s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.8s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 15.9s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 20.6s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 218ms
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.8s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.4s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.4s
mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.8s
mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 13.6s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.6s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 2: clicking GitHub card opens the install modal with correct fields 5.7s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 3: full install flow — fill PAT, submit, verify installed 12.9s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 4: installed GitHub server can be deleted 5.8s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 1: configure LLM, create switch-target profile, register trajectory 16.2s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch 6.8s
mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation 3.7s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › keeps the modal open on backdrop click and Escape 1.4s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to OpenAI GPT-5.5 1.6s
mock-llm-partial-stack.spec.ts › partial stack: --frontend-only › serves the frontend but returns 503 for backend routes 7.4s
mock-llm-partial-stack.spec.ts › partial stack: --backend-only › serves backend APIs but returns 503 for the frontend root 13.1s
mock-llm-partial-stack.spec.ts › partial stack: port conflict › fails with a clear error when the ingress port is occupied 103ms
mock-llm-partial-stack.spec.ts › partial stack: port conflict › starts successfully on a free port after a conflict 6.0s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › automation card sends the correct slash command to a conversation 16.6s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › direct slash command from home page triggers skill activation 13.6s
mock-llm-profile-management.spec.ts › active profile deletion + reconciliation › active profile is deletable and reconciliation activates another profile 8.6s
mock-llm-profile-management.spec.ts › same-model profile identity › chat header shows the correct profile when two profiles share the same model 15.9s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › project skill in workspace/.agents/skills/ triggers on matching keyword 13.7s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › user skill in ~/.openhands/skills/ triggers on matching keyword 13.5s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › deleting a user skill removes it from subsequent conversations 13.5s
mock-llm-ui-regressions.spec.ts › UI regressions › scopes standalone styles to the agent-server-ui shell 1.5s
mock-llm-ui-regressions.spec.ts › UI regressions › renders critic results on agent messages and finish actions 1.7s
mock-llm-ui-regressions.spec.ts › UI regressions › loads older events when scrolling up 1.7s
mock-llm-ui-regressions.spec.ts › UI regressions › selected workspace persists after navigating away and returning 2.1s
mock-llm-ui-regressions.spec.ts › UI regressions › cleared sessionStorage yields empty workspace selection 1.0s

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@neubig neubig requested a review from all-hands-bot June 12, 2026 21:55
@github-actions

Copy link
Copy Markdown
Contributor

🛑 Mock-LLM Docker E2E Test Results

35/38 passed · 3 skipped · ⚠️ 15 not run (process killed at 38/53)

Commit: 909082f6 · Workflow run · Test artifacts

Status Test Duration
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.9s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.5s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.8s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.7s
chromium › mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.1s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.8s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 762ms
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.2s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 33.5s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.3s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.4s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 7.8s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 6.3s
chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.7s
⏭️ chromium › mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 210ms
⏭️ chromium › mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 188ms
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 198ms
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.4s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.3s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.2s
chromium › mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.3s
chromium › mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 13.4s
chromium › mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.6s
chromium › mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 2: clicking GitHub card opens the install modal with correct fields 5.7s
chromium › mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 3: full install flow — fill PAT, submit, verify installed 12.7s
chromium › mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 4: installed GitHub server can be deleted 5.8s
chromium › mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 1: configure LLM, create switch-target profile, register trajectory 13.1s
chromium › mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch 6.6s
chromium › mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation 3.6s
chromium › mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › keeps the modal open on backdrop click and Escape 1.4s
chromium › mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to OpenAI GPT-5.5 1.6s
⏭️ chromium › mock-llm-partial-stack.spec.ts › partial stack: --frontend-only › serves the frontend but returns 503 for backend routes 188ms

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

all-hands-bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Mock-LLM Docker E2E Test Results

0/0 passed

Commit: 358faba2 · Workflow run

Status Test Duration

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@github-actions

Copy link
Copy Markdown
Contributor

🛑 Mock-LLM E2E Tests

13/13 passed · ⚠️ 40 not run (process killed at 13/53)

Commit: 358faba2 · Workflow run · Test artifacts

Status Test Duration
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.8s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.6s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.7s
chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.7s
chromium › mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.3s
chromium › mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.4s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.2s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.5s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.8s
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 783ms
chromium › mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.3s
chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 29.5s

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review: Add ChatGPT subscription LLM support

Taste Rating: 🟡 Acceptable

The implementation adds ChatGPT subscription authentication support using OAuth Device Code flow. The core logic is sound, and the feature integrates well with the existing settings infrastructure.

Summary

This PR introduces support for authenticating with ChatGPT Plus/Pro subscriptions to use supported Codex models without requiring an OpenAI API key. The implementation includes:

  • New subscription service and API endpoints
  • UI components for device code authentication flow
  • Schema modifications to support subscription auth type
  • Comprehensive tests for the new functionality

🟡 Minor Issue

[package-lock.json] Unintentional Change: The diff shows a license field being removed from @openhands/typescript-client. This appears unintentional — npm typically adds this field automatically during install. Consider reverting this specific change.


🟡 Suggestions

[src/api/agent-server-adapter.ts, Line 424] Silent Data Loss: The toRecord function silently converts undefined or null values to {}:

function toRecord(value: unknown): SettingsRecord {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return {};
  }
  return structuredClone(value as SettingsRecord);
}

This is pre-existing code, but it's worth noting that any caller passing undefined or null will get an empty object back with no indication of error. Consider adding a runtime check or type guard to prevent silent failures if the expected data is missing.

[src/routes/llm-settings.tsx, Lines 458-467] Model Selection Logic: The fallback logic for subscription model selection could be simplified. The double-check !fallbackSubscriptionModel in the condition is redundant since subscriptionModels?.[0] would be falsy anyway.


Test Coverage ✅

The tests for LLMSubscriptionService and the adapter modifications look comprehensive:

  • Tests cover the subscription auth flow (device start, poll, logout)
  • Adapter tests verify the correct payload construction for subscription vs API key auth
  • Mock handlers in settings-handlers.ts properly simulate the subscription endpoints

Security Assessment

🟢 LOW RISK

  • Uses OAuth Device Code flow, a standard and secure authentication mechanism
  • No sensitive credentials stored in code
  • API keys are properly handled and excluded from settings payloads
  • Device code tokens have appropriate expiration handling

Verdict

Worth merging: Core functionality is well-implemented. The minor issues above are suggestions for improvement rather than blockers.

Key Insight

The subscription authentication integrates cleanly with the existing settings infrastructure by adding a new auth_type field to distinguish between API key and subscription-based authentication, with the adapter properly sanitizing the payload based on auth type.


This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@github-actions

Copy link
Copy Markdown
Contributor

✅ Mock-LLM E2E Tests

53/53 passed

Commit: f96869da · Workflow run · Test artifacts

Status Test Duration
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 16.4s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.6s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.7s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.7s
mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.3s
mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.3s
mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 2.8s
mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.4s
mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.7s
mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 740ms
mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.2s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 25.3s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.3s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.4s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 6.1s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 6.4s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.7s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 15.9s
mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 19.6s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 182ms
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.5s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.4s
mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.7s
mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 14.2s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.5s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 2: clicking GitHub card opens the install modal with correct fields 5.8s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 3: full install flow — fill PAT, submit, verify installed 13.0s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 4: installed GitHub server can be deleted 5.8s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 1: configure LLM, create switch-target profile, register trajectory 13.2s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch 6.8s
mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation 3.7s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › keeps the modal open on backdrop click and Escape 1.4s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to OpenAI GPT-5.5 1.6s
mock-llm-partial-stack.spec.ts › partial stack: --frontend-only › serves the frontend but returns 503 for backend routes 7.3s
mock-llm-partial-stack.spec.ts › partial stack: --backend-only › serves backend APIs but returns 503 for the frontend root 13.1s
mock-llm-partial-stack.spec.ts › partial stack: port conflict › fails with a clear error when the ingress port is occupied 95ms
mock-llm-partial-stack.spec.ts › partial stack: port conflict › starts successfully on a free port after a conflict 6.0s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › automation card sends the correct slash command to a conversation 16.5s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › direct slash command from home page triggers skill activation 13.5s
mock-llm-profile-management.spec.ts › active profile deletion + reconciliation › active profile is deletable and reconciliation activates another profile 8.5s
mock-llm-profile-management.spec.ts › same-model profile identity › chat header shows the correct profile when two profiles share the same model 15.2s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › project skill in workspace/.agents/skills/ triggers on matching keyword 13.7s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › user skill in ~/.openhands/skills/ triggers on matching keyword 13.8s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › deleting a user skill removes it from subsequent conversations 13.5s
mock-llm-ui-regressions.spec.ts › UI regressions › scopes standalone styles to the agent-server-ui shell 1.4s
mock-llm-ui-regressions.spec.ts › UI regressions › renders critic results on agent messages and finish actions 1.4s
mock-llm-ui-regressions.spec.ts › UI regressions › loads older events when scrolling up 1.6s
mock-llm-ui-regressions.spec.ts › UI regressions › selected workspace persists after navigating away and returning 2.0s
mock-llm-ui-regressions.spec.ts › UI regressions › cleared sessionStorage yields empty workspace selection 947ms

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@github-actions

Copy link
Copy Markdown
Contributor

🔶 Mock-LLM Docker E2E Test Results

48/53 passed · 5 skipped

Commit: f96869da · Workflow run · Test artifacts

Status Test Duration
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 1: configure ACP agent via Settings → Agent UI 13.7s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 2: reload and verify ACP settings are persisted in UI 5.5s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply 6.8s
mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 4: resume ACP conversation from sidebar after navigating away 5.7s
mock-llm-auth-modes.spec.ts › auth mode: fresh install with runtime-injected key › reaches the onboarding modal without pre-seeded localStorage 1.3s
mock-llm-auth-modes.spec.ts › auth mode: non-public key rotation › recovers when localStorage has a stale session API key 5.3s
mock-llm-auth-modes.spec.ts › auth mode: public gate › shows the auth screen when no key is configured 1.1s
mock-llm-auth-modes.spec.ts › auth mode: public gate › rejects an incorrect key with an inline error 1.9s
mock-llm-auth-modes.spec.ts › auth mode: public gate › allows access after pasting the correct key 1.8s
mock-llm-auth-modes.spec.ts › auth mode: public gate › skips auth screen for returning user with valid stored key 738ms
mock-llm-auth-modes.spec.ts › auth mode: public gate › re-prompts when the server rotates its key (stale localStorage) 1.5s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 1: setup LLM profile and register automation trajectory 7.1s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI 33.4s
mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 3: verify automation and run on the automations page 6.1s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server 6.2s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 2: activate the mock-llm profile and verify settings API 6.2s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM 6.3s
mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 4: resume conversation from sidebar after navigating away 5.7s
⏭️ mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → backend-only › frontend-only connects to a separate backend-only instance 198ms
⏭️ mock-llm-cross-connect.spec.ts › cross-connect: frontend-only → multiple backends › connects to two separate backends and switches between them 196ms
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 1: ensure mock LLM profile is configured 190ms
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata 11.4s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 3: git control bar shows workspace pill and git actions 25.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 4: files tab defaults to diff view for attached workspace 5.9s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 5: browser tab shows empty state 6.3s
mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 6: files tab defaults to file-tree view without attached workspace 7.2s
mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir 7.3s
mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call 13.9s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 1: GitHub card is visible on the MCP marketplace page 5.5s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 2: clicking GitHub card opens the install modal with correct fields 5.7s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 3: full install flow — fill PAT, submit, verify installed 12.9s
mock-llm-mcp-github.spec.ts › MCP GitHub server install flow › step 4: installed GitHub server can be deleted 5.8s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 1: configure LLM, create switch-target profile, register trajectory 13.1s
mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch 6.7s
mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation 3.5s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › keeps the modal open on backdrop click and Escape 1.4s
mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to OpenAI GPT-5.5 1.6s
⏭️ mock-llm-partial-stack.spec.ts › partial stack: --frontend-only › serves the frontend but returns 503 for backend routes 185ms
mock-llm-partial-stack.spec.ts › partial stack: --backend-only › serves backend APIs but returns 503 for the frontend root 25.1s
⏭️ mock-llm-partial-stack.spec.ts › partial stack: port conflict › fails with a clear error when the ingress port is occupied 0ms
⏭️ mock-llm-partial-stack.spec.ts › partial stack: port conflict › starts successfully on a free port after a conflict 2ms
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › automation card sends the correct slash command to a conversation 15.8s
mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › direct slash command from home page triggers skill activation 13.3s
mock-llm-profile-management.spec.ts › active profile deletion + reconciliation › active profile is deletable and reconciliation activates another profile 8.5s
mock-llm-profile-management.spec.ts › same-model profile identity › chat header shows the correct profile when two profiles share the same model 14.9s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › project skill in workspace/.agents/skills/ triggers on matching keyword 13.4s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › user skill in ~/.openhands/skills/ triggers on matching keyword 13.3s
mock-llm-skills.spec.ts › skill loading: project, user, and deletion › deleting a user skill removes it from subsequent conversations 13.3s
mock-llm-ui-regressions.spec.ts › UI regressions › scopes standalone styles to the agent-server-ui shell 1.3s
mock-llm-ui-regressions.spec.ts › UI regressions › renders critic results on agent messages and finish actions 1.6s
mock-llm-ui-regressions.spec.ts › UI regressions › loads older events when scrolling up 1.7s
mock-llm-ui-regressions.spec.ts › UI regressions › selected workspace persists after navigating away and returning 2.6s
mock-llm-ui-regressions.spec.ts › UI regressions › cleared sessionStorage yields empty workspace selection 1.4s

Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)

@neubig neubig merged commit a5be238 into main Jun 12, 2026
17 checks passed
@neubig neubig deleted the add-llm-subscription-support branch June 12, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants