Skip to content

feat(llm): Add subscription-based authentication for OpenAI Codex models#1682

Merged
xingyaoww merged 34 commits intomainfrom
feat/openai-subscription-auth
Feb 2, 2026
Merged

feat(llm): Add subscription-based authentication for OpenAI Codex models#1682
xingyaoww merged 34 commits intomainfrom
feat/openai-subscription-auth

Conversation

@xingyaoww
Copy link
Collaborator

@xingyaoww xingyaoww commented Jan 10, 2026

Summary

This PR implements OAuth PKCE flow for authenticating with OpenAI's ChatGPT service, allowing users with ChatGPT Plus/Pro subscriptions to use Codex models without consuming API credits.

Key features:

  • OAuth PKCE flow with local callback server for secure authentication
  • Credential storage with automatic token refresh (stored in ~/.local/share/openhands/auth/)
  • LLM.subscription_login() classmethod for easy access
  • Support for multiple Codex models via ChatGPT subscription:
    • gpt-5.2-codex
    • gpt-5.2
    • gpt-5.1-codex-max
    • gpt-5.1-codex-mini

Usage:

from openhands.sdk import LLM

# First time: opens browser for OAuth login
llm = LLM.subscription_login(model="gpt-5.2-codex")

# Subsequent calls: reuses cached credentials
llm = LLM.subscription_login(model="gpt-5.2-codex")

# Force fresh login
llm = LLM.subscription_login(model="gpt-5.2-codex", force_login=True)

Implementation inspired by: opencode's implementation

New modules:

  • openhands/sdk/llm/auth/__init__.py - Auth module exports
  • openhands/sdk/llm/auth/credentials.py - Credential storage and retrieval
  • openhands/sdk/llm/auth/openai.py - OpenAI OAuth PKCE flow implementation

Checklist

  • If the PR is changing/adding functionality, are there tests to reflect this?
  • If there is an example, have you run the example to make sure that it works?
  • If there are instructions on how to run the code, have you followed the instructions and made sure that it works?
  • If the feature is significant enough to require documentation, is there a PR open on the OpenHands/docs repository with the same branch name?
  • Is the github CI passing?

Note: This feature requires a ChatGPT Plus/Pro subscription to test the actual OAuth flow. The unit tests cover the credential storage, PKCE generation, URL building, and mock token refresh scenarios.

@xingyaoww can click here to continue refining the PR


Agent Server images for this PR

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

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:e8278f7-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-e8278f7-python \
  ghcr.io/openhands/agent-server:e8278f7-python

All tags pushed for this build

ghcr.io/openhands/agent-server:e8278f7-golang-amd64
ghcr.io/openhands/agent-server:e8278f7-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:e8278f7-golang-arm64
ghcr.io/openhands/agent-server:e8278f7-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:e8278f7-java-amd64
ghcr.io/openhands/agent-server:e8278f7-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:e8278f7-java-arm64
ghcr.io/openhands/agent-server:e8278f7-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:e8278f7-python-amd64
ghcr.io/openhands/agent-server:e8278f7-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-amd64
ghcr.io/openhands/agent-server:e8278f7-python-arm64
ghcr.io/openhands/agent-server:e8278f7-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-arm64
ghcr.io/openhands/agent-server:e8278f7-golang
ghcr.io/openhands/agent-server:e8278f7-java
ghcr.io/openhands/agent-server:e8278f7-python

About Multi-Architecture Support

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

Implement OAuth PKCE flow for authenticating with OpenAI's ChatGPT service,
allowing users with ChatGPT Plus/Pro subscriptions to use Codex models
(gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex-mini) without
consuming API credits.

Key features:
- OAuth PKCE flow with local callback server for secure authentication
- Credential storage with automatic token refresh
- LLM.subscription_login() classmethod for easy access
- Support for multiple Codex models via ChatGPT subscription

Usage:
  from openhands.sdk import LLM
  llm = LLM.subscription_login(model='gpt-5.2-codex')

Inspired by opencode's implementation (anomalyco/opencode#7537).

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

github-actions bot commented Jan 10, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/llm
   llm.py4458680%391, 412–413, 449, 626, 727, 729–730, 758, 808, 819–821, 825–829, 837–839, 849–851, 854–855, 859, 861–862, 864, 887–892, 1012, 1017–1018, 1183–1184, 1193, 1206, 1208–1213, 1215–1232, 1235–1239, 1241–1242, 1248–1257, 1308, 1310
openhands-sdk/openhands/sdk/llm/auth
   credentials.py63198%108
   openai.py26610660%173–174, 178–180, 212, 215–216, 218–220, 222–223, 228, 230–234, 258–259, 270–272, 277–278, 287–289, 414–417, 420, 423, 425–426, 428–432, 437–442, 448–452, 458–459, 462–468, 474, 476–478, 480–485, 491, 493, 495–497, 499, 503–504, 507–508, 512, 515, 521–523, 526, 581, 583, 592, 647–648, 652, 655–659, 662–664, 667–668, 683, 721
TOTAL16855494170% 

- Use authlib for PKCE generation (generate_token, create_s256_code_challenge)
- Use aiohttp.web for OAuth callback server (cleaner than raw asyncio)
- Add Codex-specific parameters (store=false, instructions) via litellm_extra_body
- Add max_output_tokens=None for Codex (not supported by the API)
- Reduce code from ~575 lines to ~492 lines

This addresses the 'Instructions are not valid' error by properly
configuring the Codex API parameters.

Co-authored-by: openhands <openhands@all-hands.dev>
@xingyaoww
Copy link
Collaborator Author

@OpenHands /codereview-roasted and help me fix all the issues and make sure pre-commit run --all-files pass

@openhands-ai
Copy link

openhands-ai bot commented Jan 20, 2026

I'm on it! xingyaoww can track my progress at all-hands.dev

- Move time import to module level in credentials.py
- Replace os.uname() with platform.system()/machine() for cross-platform compatibility
- Move urllib.parse.urlencode import to module level in openai.py
- Remove extra blank line before __all__ in auth/__init__.py
- Apply ruff formatting fixes to llm.py

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

openhands-ai bot commented Jan 20, 2026

Summary

I completed the roasted code review and fixed all identified issues. The changes have been pushed to the feat/openai-subscription-auth branch.

Code Review (Linus-Style) 🔴 Needs Improvement

Critical Issues Fixed:

  1. [credentials.py] time module was imported inside methods instead of at module level - violates Python best practices
  2. [openai.py] os.uname() is Unix-only and would break on Windows - replaced with cross-platform platform.system()/platform.machine()
  3. [openai.py] urllib.parse.urlencode was imported inside a function - moved to top-level imports

Style Fixes:
4. [llm.py] Applied ruff formatting fixes for line length consistency

Changes Made (3 files, -6 lines net)

File Change
credentials.py Moved import time to module level, removed 2 inline imports
openai.py Replaced os with platform module, moved urlencode import to top
llm.py Ruff formatting fixes (auto-applied by pre-commit)

Verification ✅

  • All pre-commit hooks pass
  • All 25 auth tests pass
  • All 9 serialization tests pass

The PR is now updated at: #1682

View full conversation

openhands-agent and others added 4 commits January 20, 2026 02:10
Refactor the inline subscription transport check to a reusable @Property
method on the LLM class. This improves code readability and allows the
subscription check to be used elsewhere if needed.

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

- Add _is_subscription private attribute to LLM class (default False)
- Update is_subscription property to use the attribute directly
- Set _is_subscription=True in OpenAISubscriptionAuth.create_llm()
- Update tests to explicitly set _is_subscription for subscription tests
- Add example file for subscription login usage

This removes the URL-based heuristics and ensures is_subscription only
returns True for LLMs created via LLM.subscription_login().

Co-authored-by: openhands <openhands@all-hands.dev>
- Hardcode stream=True, temperature=None, max_output_tokens=None in subscription_login
- Skip unsupported parameters in responses_options for subscription mode
- Force parameters to None after LLM init to prevent auto-detection from model info

Fixes API errors: 'Unsupported parameter: max_output_tokens', 'Unsupported parameter: temperature', 'Stream must be set to true'

Co-authored-by: openhands <openhands@all-hands.dev>
@xingyaoww
Copy link
Collaborator Author

@OpenHands review diff against main, understand the purpose of this PR, then open a new PR in OpenHands/docs repo to document the new feature added in this PR. The doc should appear under sdk/guides/llm_subscriptions.mdx - please follow the format of other LLM provider, keep in mind that OpenAI subscription is the first provider we supported, we will open support for other subscriptions in the future

@openhands-ai
Copy link

openhands-ai bot commented Jan 20, 2026

I'm on it! xingyaoww can track my progress at all-hands.dev

Add a Literal type alias SupportedVendor = Literal["openai"] for the
vendor parameter in subscription_login methods. This provides better
type safety and makes it easier to add more vendors in the future.

Changes:
- Define SupportedVendor type in openhands/sdk/llm/auth/openai.py
- Export SupportedVendor from openhands/sdk/llm/auth/__init__.py
- Update vendor parameter type in:
  - LLM.subscription_login()
  - subscription_login()
  - subscription_login_async()

Co-authored-by: openhands <openhands@all-hands.dev>
xingyaoww pushed a commit to OpenHands/docs that referenced this pull request Jan 20, 2026
…hentication

This adds documentation for the new subscription_login() feature that allows
users to authenticate with their ChatGPT Plus/Pro subscription to access
Codex models without consuming API credits.

Key features documented:
- OAuth PKCE authentication flow
- Credential caching and automatic token refresh
- Supported models (gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex-mini)
- Configuration options (force_login, open_browser)
- Credential storage location

Related SDK PR: OpenHands/software-agent-sdk#1682

Co-authored-by: openhands <openhands@all-hands.dev>
- Extract subscription-specific message transformation into dedicated methods
  (_transform_for_subscription, _inject_system_prefix)
- Use list comprehension for message normalization
- Consolidate create_llm method in OpenAISubscriptionAuth
- Reduce code duplication and improve readability
- Fix test assertion to match actual originator header value

Co-authored-by: openhands <openhands@all-hands.dev>
@xingyaoww
Copy link
Collaborator Author

@OpenHands please fix the failing actions on PR #1682 at branch feat/openai-subscription-auth

Review diff from main, understand the purpose of this PR, and help me update OpenHands/docs#267 with the latest state of this PR

@openhands-ai
Copy link

openhands-ai bot commented Feb 1, 2026

I'm on it! xingyaoww can track my progress at all-hands.dev

@openhands-ai
Copy link

openhands-ai bot commented Feb 1, 2026

Summary

I've successfully fixed the failing GitHub Actions on PR #1682 and updated the docs PR #267.

Problem Identified

The CI workflow "[Optional] Docs example" was failing because two SDK examples were undocumented:

  1. examples/01_standalone_sdk/34_critic_example.py
  2. examples/01_standalone_sdk/35_subscription_login.py

Changes Made to OpenHands/docs PR #267

1. Fixed sdk/guides/llm-subscriptions.mdx:

  • Updated example path from 34_subscription_login.py to 35_subscription_login.py
  • Added vendor="openai" parameter to all code examples to match the actual SDK example
  • Updated the code block to match the actual example structure

2. Created new sdk/guides/critic.mdx:

  • Added documentation for the experimental critic-based evaluation feature
  • Includes the full example code from 34_critic_example.py
  • Documents the APIBasedCritic class and auto-configuration for All-Hands LLM proxy
  • Added configuration options and environment variables

3. Updated docs.json:

  • Added sdk/guides/critic to the "Agent Features" navigation group

Verification

  • The "[Optional] Docs example" CI workflow is now passing (conclusion: success)
  • Changes have been pushed to the feat/openai-subscription-auth branch on OpenHands/docs

Links

View full conversation

@xingyaoww
Copy link
Collaborator Author

Tested and confirm it works - gonna merge it for next release

@xingyaoww xingyaoww closed this Feb 2, 2026
@xingyaoww xingyaoww reopened this Feb 2, 2026
@xingyaoww xingyaoww enabled auto-merge (squash) February 2, 2026 05:47
@xingyaoww xingyaoww disabled auto-merge February 2, 2026 05:48
- Remove CONSENT_DISCLAIMER constant and related verbose display logic
- Simplify _display_consent_and_confirm() function by removing verbose parameter
- Remove verbose parameter from subscription_login_async and subscription_login
- Update tests to reflect the simplified consent flow
@xingyaoww
Copy link
Collaborator Author

@OpenHands please fix the failing actions on PR #1682 at branch feat/openai-subscription-auth

Do not change implementation, just fix the tests

@openhands-ai
Copy link

openhands-ai bot commented Feb 2, 2026

I'm on it! xingyaoww can track my progress at all-hands.dev

…mentation

The tests were expecting ~/.local/share/openhands/auth but the implementation
uses ~/.openhands/auth. Updated tests to match the actual implementation.

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

openhands-ai bot commented Feb 2, 2026

Summary

I fixed the failing tests in PR #1682 on branch feat/openai-subscription-auth.

Issue

Two tests in tests/sdk/llm/auth/test_credentials.py were failing:

  • test_get_credentials_dir_default - expected ~/.local/share/openhands/auth
  • test_get_credentials_dir_xdg - expected $XDG_DATA_HOME/openhands/auth

But the actual implementation in credentials.py returns ~/.openhands/auth regardless of environment variables.

Fix

Updated the tests to match the actual implementation (as requested - no implementation changes):

  1. test_get_credentials_dir_default - now expects ~/.openhands/auth
  2. test_get_credentials_dir_xdg - now expects ~/.openhands/auth (with updated docstring clarifying that XDG_DATA_HOME is ignored)

Verification

  • ✅ All 11 tests in test_credentials.py pass locally
  • ✅ Pre-commit hooks pass
  • ✅ Changes pushed to feat/openai-subscription-auth branch

The CI should now pass for the sdk-tests job.

View full conversation

@openhands-ai
Copy link

openhands-ai bot commented Feb 2, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Run tests

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1682 at branch `feat/openai-subscription-auth`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

@enyst
Copy link
Collaborator

enyst commented Feb 2, 2026

Yes, it does work! It looks fairly good to me. I'm very glad that the vast majority of the code is in separate files, so we can find it easy to make it into an extension later.

@OpenHands Look at the entire branch including responses() API path in llm.py, and the file openai.py, and tell me:

  • what exactly are the instructions sent to the LLM in the subscription mode
  • what exactly is prepended to the first user message

Investigate the entire instructions and system message event, including suffixes.

Do not make any modifications, I just want the complete answer, post it as comment on github here, use remote gh. Note that it will be rendered as markdown.

@openhands-ai
Copy link

openhands-ai bot commented Feb 2, 2026

I'm on it! enyst can track my progress at all-hands.dev

@enyst
Copy link
Collaborator

enyst commented Feb 2, 2026

Subscription mode: instructions sent to LLM (Responses API)

  • LLM.responses()format_messages_for_responses()transform_for_subscription().
  • In subscription mode, transform_for_subscription() always returns the instructions string as:
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.

(defined as DEFAULT_SYSTEM_MESSAGE in openhands/sdk/llm/auth/openai.py).

Subscription mode: what is prepended to the first user message

  • All system prompts are gathered as system_chunks (each is Message.to_responses_value(...).strip()).
  • They are merged with the separator "\n\n---\n\n":
merged = "\n\n---\n\n".join(system_chunks)
  • The exact prefix content prepended to the first user message is:
Context (system prompt):
{merged}


(note the trailing blank line; this is the literal f"Context (system prompt):\n{merged}\n\n").

  • This prefix is inserted as an input_text content item before any existing user content. If no user message exists, a synthetic user message is created with only this prefix.

Notes on suffixes / separators

  • The only separator between multiple system messages is \n\n---\n\n.
  • No other suffix is appended to the instructions string in subscription mode (instructions are always the single DEFAULT_SYSTEM_MESSAGE).

@openhands-ai

This comment was marked as duplicate.

@enyst
Copy link
Collaborator

enyst commented Feb 2, 2026

Skills / AgentSkills “system suffix” behavior in subscription mode

  • The skills/AgentSkills system suffix is generated by AgentContext.get_system_message_suffix() using context/prompts/templates/system_message_suffix.j2 (repo skills + available skills + custom suffix + secrets), and appended to the system message in AgentBase.system_message().
  • In subscription Responses mode, all system messages (including that skills suffix) are collected into system_chunks and then prepended to the first user message by transform_for_subscription() as:
Context (system prompt):
{merged_system_chunks}


  • So: the skills suffix is included, but it’s no longer a literal system-message suffix in the request — it’s part of the prepended system context on the first user message.
  • Triggered knowledge skills are appended to the user message via AgentContext.get_user_message_suffix() before the subscription transform, so they pass through unchanged.

@xingyaoww xingyaoww merged commit d85241b into main Feb 2, 2026
25 checks passed
@xingyaoww xingyaoww deleted the feat/openai-subscription-auth branch February 2, 2026 13:54
@enyst
Copy link
Collaborator

enyst commented Feb 2, 2026

Tools + system prompt event (concise)

  • Tool specs stay in the tools field of the Responses request; they are not prepended to system/user text in subscription mode.
  • The system prompt event saved on disk (SystemPromptEvent) stores the full system message text (system_prompt=TextContent(text=self.system_message), which already includes the AgentContext/system‑suffix content like skills/available skills/secrets) plus the tool definitions list (tools=[ToolDefinition...]).
    • Source: openhands/sdk/agent/agent.pySystemPromptEvent(...).

xingyaoww added a commit to OpenHands/docs that referenced this pull request Feb 2, 2026
…hentication (#267)

* docs: Add LLM subscriptions guide for OpenAI ChatGPT subscription authentication

This adds documentation for the new subscription_login() feature that allows
users to authenticate with their ChatGPT Plus/Pro subscription to access
Codex models without consuming API credits.

Key features documented:
- OAuth PKCE authentication flow
- Credential caching and automatic token refresh
- Supported models (gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex-mini)
- Configuration options (force_login, open_browser)
- Credential storage location

Related SDK PR: OpenHands/software-agent-sdk#1682

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

* docs: Update subscription login example path and add critic documentation

- Fix subscription_login example path from 34 to 35
- Add vendor='openai' parameter to all code examples
- Update code to match actual SDK example
- Add new critic.mdx documentation for 34_critic_example.py
- Add critic page to Agent Features navigation group

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

* docs: Update LLM subscriptions guide and add ChatGPT login option to quickstart

- Update credential storage path from ~/.local/share/openhands/auth/ to ~/.openhands/auth/ to match SDK PR #1682
- Fix example directory name from agent-sdk to software-agent-sdk
- Add ChatGPT Subscription as third option in how-to-run-example.mdx
- Add Tip component linking to LLM Subscriptions guide

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

* Apply suggestion from @xingyaoww

* docs: Simplify ChatGPT subscription option to Tip with link

Remove code block for ChatGPT subscription option and keep it as a Tip
component that links to the LLM Subscriptions guide.

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

* docs: Refactor llm-subscriptions.mdx to follow guide format

- Add import for RunExampleCode shared snippet
- Add 'ready-to-run example' link at top
- Restructure 'How It Works' section using Steps/Step components
- Move 'Ready-to-run Example' section to end (before Next Steps)
- Add RunExampleCode component after code block
- Add icon='python' to all code blocks

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

* docs: Simplify llm-subscriptions.mdx - remove technical details

- Remove OAuth PKCE flow details (keep it high-level)
- Remove Automatic Token Management step (merge into step 2)
- Simplify Credential Storage section

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

* docs: Add ChatGPT subscription option to getting-started and update AGENTS.md

- Add Option 3: ChatGPT Subscription to getting-started.mdx AccordionGroup
- Add 'LLM API Key Options' section to AGENTS.md documenting the three ways
  users can obtain LLM access for future reference

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

* Update sdk/guides/llm-subscriptions.mdx

Co-authored-by: Engel Nyst <engel.nyst@gmail.com>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
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.

6 participants