Bug Description
Serialized ChatGPT subscription LLM profiles can reload as incomplete runtime objects. LLM._restore_is_subscription restores _is_subscription=True, but runtime-only OAuth credentials, the Codex base_url, and transport headers are not serialized. create_subscription_llm_from_config currently returns early for any llm.is_subscription, so the incomplete object is never rebuilt.
The resulting LLM sends Responses API requests to https://api.openai.com/v1/responses instead of the ChatGPT Codex endpoint and fails with Missing scopes: api.responses.write.
Related Agent Canvas reports:
Expected Behavior
Loading a persisted subscription LLM should rebuild a complete runtime subscription transport with:
base_url=https://chatgpt.com/backend-api/codex
- OAuth credentials attached at runtime
- Codex request headers restored
- the provider-qualified model name
An already-initialized in-process runtime LLM should remain unchanged.
Actual Behavior
LLM.from_persisted() restores is_subscription=True, then create_subscription_llm_from_config() returns the object unchanged because of:
if llm.is_subscription:
return llm
The persisted object has base_url=None and no runtime credentials. A model call then reaches the normal OpenAI Platform endpoint and fails:
litellm.BadRequestError: OpenAIException -
You have insufficient permissions for this operation.
Missing scopes: api.responses.write.
Steps to Reproduce
from openhands.sdk import LLM
from openhands.sdk.llm.auth.credentials import OAuthCredentials
from openhands.sdk.llm.auth.openai import OpenAISubscriptionAuth
credentials = OAuthCredentials(
vendor="openai",
access_token="access-token",
refresh_token="refresh-token",
expires_at=4_102_444_800_000,
)
source = OpenAISubscriptionAuth().create_llm(
model="gpt-5.6",
credentials=credentials,
)
persisted = source.to_persisted()
loaded = LLM.from_persisted(persisted)
assert persisted["is_subscription"] is True
assert "base_url" not in persisted
assert loaded.base_url == "https://chatgpt.com/backend-api/codex"
The final assertion fails on current main because loaded.base_url is None.
Installation Method
Editable monorepo checkout using uv sync --dev.
SDK Version
main at release 1.35.0 plus commit 39f404d ancestry; reproduced on current upstream main on 2026-07-11.
Python Version
Python 3.13.12. The defect is serialization/runtime-state logic and is not Windows-specific.
Proposed Fix
Only preserve an existing subscription LLM when runtime credentials are actually attached:
if llm.is_subscription and llm._subscription_credentials is not None:
return llm
Otherwise rebuild through OpenAISubscriptionAuth.create_llm(...). Also exclude the computed is_subscription field from reconstruction kwargs.
A regression test should cover the complete create_llm() -> to_persisted() -> from_persisted() round trip and assert restoration of the Codex URL, OAuth credentials, and headers.
Existing Issue Search
Bug Description
Serialized ChatGPT subscription LLM profiles can reload as incomplete runtime objects.
LLM._restore_is_subscriptionrestores_is_subscription=True, but runtime-only OAuth credentials, the Codexbase_url, and transport headers are not serialized.create_subscription_llm_from_configcurrently returns early for anyllm.is_subscription, so the incomplete object is never rebuilt.The resulting LLM sends Responses API requests to
https://api.openai.com/v1/responsesinstead of the ChatGPT Codex endpoint and fails withMissing scopes: api.responses.write.Related Agent Canvas reports:
gpt-5.1-codex-maxagent-canvas#1557Expected Behavior
Loading a persisted subscription LLM should rebuild a complete runtime subscription transport with:
base_url=https://chatgpt.com/backend-api/codexAn already-initialized in-process runtime LLM should remain unchanged.
Actual Behavior
LLM.from_persisted()restoresis_subscription=True, thencreate_subscription_llm_from_config()returns the object unchanged because of:The persisted object has
base_url=Noneand no runtime credentials. A model call then reaches the normal OpenAI Platform endpoint and fails:Steps to Reproduce
The final assertion fails on current
mainbecauseloaded.base_urlisNone.Installation Method
Editable monorepo checkout using
uv sync --dev.SDK Version
mainat release 1.35.0 plus commit39f404dancestry; reproduced on current upstreammainon 2026-07-11.Python Version
Python 3.13.12. The defect is serialization/runtime-state logic and is not Windows-specific.
Proposed Fix
Only preserve an existing subscription LLM when runtime credentials are actually attached:
Otherwise rebuild through
OpenAISubscriptionAuth.create_llm(...). Also exclude the computedis_subscriptionfield from reconstruction kwargs.A regression test should cover the complete
create_llm() -> to_persisted() -> from_persisted()round trip and assert restoration of the Codex URL, OAuth credentials, and headers.Existing Issue Search
OpenHands/software-agent-sdkand found no duplicate.