Skip to content

fix(llm): serialize is_subscription so subscription mode survives remote agent-server transport#3633

Open
xingyaoww wants to merge 1 commit into
mainfrom
fix/serialize-is-subscription
Open

fix(llm): serialize is_subscription so subscription mode survives remote agent-server transport#3633
xingyaoww wants to merge 1 commit into
mainfrom
fix/serialize-is-subscription

Conversation

@xingyaoww

@xingyaoww xingyaoww commented Jun 10, 2026

Copy link
Copy Markdown
Member
  • A human has tested these changes.

AGENT:


Why

is_subscription was a plain @property backed by the _is_subscription PrivateAttr, so it was silently dropped whenever an LLM was serialized — in particular when a RemoteConversation ships the agent's LLM to an agent-server. The server rebuilds the LLM with is_subscription == False, and every subscription-specific behavior stops applying server-side:

  • the streaming on_token exemption in responses() (raises ValueError: Streaming requires an on_token callback),
  • the Codex system-prompt transform (transform_for_subscription),
  • reasoning-item stripping for store=false (follow-up requests reference unresolvable item IDs → 404 from the Codex endpoint).

Net effect: LLM.subscription_login() works locally but silently breaks with remote workspaces.

Summary

  • Promote the is_subscription property to a @computed_field so it is included in model_dump().
  • Keep the backing _is_subscription PrivateAttr and its existing write path untouched (no call-site or API changes; OpenAISubscriptionAuth.create_llm still sets it directly).
  • Add a wrap model_validator that restores _is_subscription when validating serialized data, so the flag survives a dump → validate round trip (e.g. transport to a remote agent-server).

Issue Number

N/A

How to Test

Round-trip is the core of the fix:

from openhands.sdk import LLM

llm = LLM(model="openai/gpt-5.2-codex", base_url="https://chatgpt.com/backend-api/codex")
llm._is_subscription = True
restored = LLM.model_validate(llm.model_dump(context={"expose_secrets": True}))
assert restored.is_subscription is True          # was False before this PR

plain = LLM(model="gpt-4o")
assert LLM.model_validate(plain.model_dump(context={"expose_secrets": True})).is_subscription is False

Automated:

uv run pytest tests/sdk/llm/test_subscription_mode.py tests/sdk -q   # 3535 passed
uv run python .github/scripts/check_sdk_api_breakage.py              # No breaking changes detected
uv run --with packaging python .github/scripts/check_agent_server_rest_api_breakage.py  # No breaking changes detected (exit 0)

A new regression test (test_is_subscription_survives_serialization_round_trip) covers both the subscription and plain cases. ruff + pyright clean on the touched files.

Video/Screenshots

N/A (no UI surface).

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

is_subscription becomes an additive, read-only computed field in the serialized schema; the griffe API-breakage check sees no ATTRIBUTE_CHANGED_VALUE (still a property), and the REST schema gains only the additive read-only field. Backward compatible: old payloads without the field validate to False as before.

🤖 Generated with Claude Code


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-slim 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:2daa3f8-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:2daa3f8-golang-amd64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-golang-amd64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-golang-amd64
ghcr.io/openhands/agent-server:2daa3f8-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:2daa3f8-golang-arm64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-golang-arm64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-golang-arm64
ghcr.io/openhands/agent-server:2daa3f8-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:2daa3f8-java-amd64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-java-amd64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-java-amd64
ghcr.io/openhands/agent-server:2daa3f8-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:2daa3f8-java-arm64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-java-arm64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-java-arm64
ghcr.io/openhands/agent-server:2daa3f8-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:2daa3f8-python-amd64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-python-amd64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-python-amd64
ghcr.io/openhands/agent-server:2daa3f8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:2daa3f8-python-arm64
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-python-arm64
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-python-arm64
ghcr.io/openhands/agent-server:2daa3f8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:2daa3f8-golang
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-golang
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-golang
ghcr.io/openhands/agent-server:2daa3f8-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:2daa3f8-java
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-java
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-java
ghcr.io/openhands/agent-server:2daa3f8-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:2daa3f8-python
ghcr.io/openhands/agent-server:2daa3f83fd8ad4745e04f1740a1028512979d55c-python
ghcr.io/openhands/agent-server:fix-serialize-is-subscription-python
ghcr.io/openhands/agent-server:2daa3f8-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 2daa3f8-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., 2daa3f8-python-amd64) are also available if needed

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/llm
   llm.py84813284%532, 548, 581–582, 893–894, 897–901, 903, 911–913, 917, 934–935, 939, 941–942, 944–946, 1069, 1192, 1385, 1394–1396, 1495, 1506, 1547, 1559–1561, 1564–1567, 1573, 1631, 1642, 1685, 1698–1700, 1703–1706, 1712, 1891–1896, 2012–2013, 2353–2354, 2363, 2381, 2405–2406, 2408, 2410, 2412, 2420, 2423, 2425, 2427, 2435, 2439–2440, 2450–2454, 2458, 2462–2463, 2468, 2472, 2478, 2483, 2524, 2526–2531, 2533–2550, 2553–2557, 2559–2560, 2566–2575, 2632, 2634
TOTAL29932840571% 

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ QA Report: PASS WITH ISSUES

Functional QA confirms the PR preserves subscription mode across SDK serialization boundaries; CI is not fully green at the time of review.

Does this PR achieve its stated goal?

Yes. The PR set out to make is_subscription survive serialization so a remote agent-server rebuild keeps subscription-specific behavior. I exercised the SDK as a user would by constructing subscription LLM/Agent objects, crossing a JSON model_dumpmodel_validate transport boundary, and reusing the restored object; base loses the flag, while this PR preserves it and keeps subscription message formatting active after restore.

Phase Result
Environment Setup make build completed successfully
CI Status ⚠️ REST check summary: 20 success, 2 failure (Python API, Validate PR description), 8 in progress, 1 skipped
Functional Verification ✅ Base reproduces flag loss; PR preserves is_subscription through LLM and Agent payloads
Functional Verification

Test 1: LLM remote-style serialization keeps subscription behavior

Step 1 — Reproduce baseline without the fix:
Ran git checkout --quiet origin/main && uv run python /tmp/qa_subscription_transport.py:

payload_has_is_subscription=False
payload_is_subscription=None
restored_is_subscription=False
plain_restored_is_subscription=False
instructions='SYSTEM'
inputs_contain_system_text=False
inputs_contain_user_text=True

This confirms the bug: a subscription-marked LLM loses the flag after JSON serialization/rebuild, so the restored object behaves as non-subscription.

Step 2 — Apply the PR's changes:
Checked out fix/serialize-is-subscription at d672c782056d15576e3636054d31855555dbbd65.

Step 3 — Re-run with the fix in place:
Ran uv run python /tmp/qa_subscription_transport.py:

payload_has_is_subscription=True
payload_is_subscription=True
restored_is_subscription=True
plain_restored_is_subscription=False
instructions='You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.'
inputs_contain_system_text=True
inputs_contain_user_text=True

This shows the serialized payload now carries is_subscription=True, the rebuilt LLM keeps it, a plain LLM still defaults to False, and subscription-specific message formatting remains active after the transport boundary.

Test 2: Agent payload preserves the nested subscription LLM

Step 1 — Reproduce baseline without the fix:
Ran git checkout --quiet origin/main && uv run python /tmp/qa_agent_transport.py:

agent_payload_llm_has_is_subscription=False
agent_payload_llm_is_subscription=None
restored_agent_llm_is_subscription=False

This confirms the remote-agent payload path loses the nested LLM subscription flag on base.

Step 2 — Apply the PR's changes:
Checked out fix/serialize-is-subscription at d672c782056d15576e3636054d31855555dbbd65.

Step 3 — Re-run with the fix in place:
Ran uv run python /tmp/qa_agent_transport.py:

agent_payload_llm_has_is_subscription=True
agent_payload_llm_is_subscription=True
restored_agent_llm_is_subscription=True

This verifies the Agent serialization boundary used by remoting now preserves the subscription LLM flag.

Unable to Verify

I did not exercise a live ChatGPT subscription/Codex request or an authenticated end-to-end RemoteConversation against a running agent-server because no ChatGPT subscription credentials were available in the QA environment. The core transport behavior was verified locally at the exact SDK serialization/rebuild boundary that remote transport uses.

Issues Found

  • 🟠 Issue: CI is not currently green: Python API and Validate PR description are failing, with 8 checks still in progress in the latest REST check snapshot.
  • No functional issues were found in the exercised subscription serialization behavior.

This review was created by an AI agent (OpenHands) on behalf of the user.

Verdict: PASS WITH ISSUES

@xingyaoww

Copy link
Copy Markdown
Member Author

Backward compatibility

Reviewed against the SDK deprecation policy (public API removals need deprecation metadata with a ≥5-minor-release runway):

  • Public read API is unchanged: llm.is_subscription reads identically (property → field).
  • The new field is additive: old payloads without it deserialize to the default False; older servers ignore the extra key via extra="ignore". The Field(default=...) release-note workflow should pick this up automatically.
  • Legacy private-attr writes keep working: there was no public setter before this PR, so code written against ≤1.26 had to poke llm._is_subscription = True directly (our own tests did). Added a __setattr__ redirect that forwards such writes to the public field and emits warn_deprecated(deprecated_in="1.27.0", removed_in="1.32.0") — without it, those writes would silently set a dead attribute and subscription handling would stop applying. Covered by a new test.
  • Searched the org's downstream consumers (OpenHands-CLI, app-server, enterprise) — no external _is_subscription usage found.

@xingyaoww xingyaoww requested a review from all-hands-bot June 10, 2026 15:44

all-hands-bot commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

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
Collaborator

Choose a reason for hiding this comment

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

⚠️ QA Report: PASS WITH ISSUES

Functional QA confirms is_subscription now survives SDK serialization and local agent-server transport; the only issue noted is current CI status (API breakage checks are failing).

Does this PR achieve its stated goal?

Yes, functionally. On origin/main, a subscription-marked LLM lost the flag during model_dump()model_validate() (serialized_has_is_subscription=False, restored_is_subscription=False). With this PR, the same SDK path preserves it (serialized_has_is_subscription=True, restored_is_subscription=True), and a local agent-server created through RemoteConversation returned agent.llm.is_subscription=True from /api/conversations/{id}.

Phase Result
Environment Setup make build completed successfully and installed the editable SDK/server packages.
CI Status ⚠️ Current snapshot: 17 success, 1 skipped, 2 failing (Python API, REST API (OpenAPI)), 9 in progress.
Functional Verification ✅ Real SDK calls plus a local agent-server transport path preserved subscription mode.
Functional Verification

Test 1: Reproduce serialization loss, then verify PR preserves the flag

Step 1 — Reproduce / establish baseline (without the fix):
Ran git checkout --detach origin/main && OPENHANDS_SUPPRESS_BANNER=1 VIRTUAL_ENV= uv run python - <<'PY' ... PY to create a subscription-marked LLM, serialize it, rebuild it, and format response messages:

branch_ref=origin/main
original_is_subscription=True
serialized_has_is_subscription=False
restored_is_subscription=False
instructions_is_none=False
formatted_inputs=[{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "USER"}]}]

This confirms the bug exists on the base branch: the original object was in subscription mode, but the serialized payload did not include the flag and the rebuilt object reverted to non-subscription behavior.

Step 2 — Apply the PR's changes:
Checked out PR commit 3b118a994d72bbc0313a5be031a610d20dfdeb64.

Step 3 — Re-run with the fix in place:
Ran the equivalent SDK script with llm.is_subscription = True:

branch_ref=PR 3b118a9
original_is_subscription=True
serialized_has_is_subscription=True
serialized_is_subscription=True
restored_is_subscription=True
instructions_is_none=False
formatted_inputs=[{"role": "user", "content": [{"type": "input_text", "text": "Context (system prompt):\nSYSTEM\n\n"}, {"type": "input_text", "text": "USER"}]}]

This shows the serialized transport payload now carries is_subscription=True, the rebuilt LLM keeps it, and subscription-specific message formatting still applies after the round trip.

Test 2: Exercise actual local agent-server transport

Step 1 — Establish baseline:
Test 1 established that the transport-style serialization boundary failed before the PR.

Step 2 — Apply the PR's changes and start software:
Started the local server with uv run python -m openhands.agent_server --host 127.0.0.1 --port 8123, confirmed /server_info returned HTTP 200, then created a real RemoteConversation with an Agent whose LLM had is_subscription=True.

Step 3 — Inspect server-side conversation state:
Queried GET /api/conversations/{conversation_id} from the running server:

conversation_id=fe5df9f3-2286-43ef-8125-e84266b66035
get_conversation_status=200
response_mentions_is_subscription=True
agent.llm.is_subscription=True
top_level_keys=activated_knowledge_skills,agent,agent_state,available_models,blocked_actions,blocked_messages,client_tools,confirmation_policy,created_at,current_model_id,execution_status,hook_config,id,invoked_skills,last_user_message_id,max_iterations,metrics,persistence_dir,secret_registry,security_analyzer,stats,stuck_detection,supports_runtime_model_switch,tags,title,updated_at,workspace

This verifies the PR's target remote transport path in running software: the agent-server rebuilt/stored the remote conversation with agent.llm.is_subscription=True.

Test 3: Legacy setter and plain LLM behavior

Step 1 — Baseline:
The base-branch run in Test 1 showed direct _is_subscription mutation worked only locally and did not survive serialization.

Step 2 — Re-run compatibility/default checks on the PR:
Ran a script that sets legacy._is_subscription = True, captures warnings, round-trips through serialization, and checks a plain LLM:

branch_ref=PR 3b118a9
legacy_write_warning_count=1
legacy_warning_types=DeprecatedWarning
legacy_restored_is_subscription=True
plain_serialized_is_subscription=False
plain_restored_is_subscription=False
legacy_warning_is_deprecated=True

This shows older direct writes still function with a deprecation warning, and non-subscription LLMs still default to False after serialization.

Issues Found

  • ⚠️ CI status: Python API and REST API (OpenAPI) breakage checks are currently failing in GitHub checks; I did not investigate or rerun CI jobs as part of functional QA.

This QA review was created by an AI agent (OpenHands) on behalf of the user.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review Summary

🟢 Good taste - Elegant, minimal fix that solves a real serialization problem with clean backward compatibility.

Changes Overview

This PR converts is_subscription from a private PrivateAttr to a public Field so it survives Pydantic serialization (model_dump/model_validate) when the LLM is transported to a remote agent-server.

Analysis

[CRITICAL ISSUES] - None

[IMPROVEMENT OPPORTUNITIES] - None identified

[TESTING]

  • test_is_subscription_survives_serialization_round_trip() - Verifies the core fix works as intended
  • test_legacy_private_attr_write_redirects_with_deprecation_warning() - Verifies backward compatibility with deprecation warning
  • Existing tests updated to use the public field instead of private attr

Notable Design Decisions

  1. Deprecation via __setattr__ interception - Clean approach to maintain backward compatibility while guiding users to the new API. The deprecation path is gradual (3 minor versions to removal).

  2. Documentation in field description - Good that the docstring explicitly mentions the remote transport use case.

  3. Minimal surface area - Only 4 files changed, focused change.

[RISK ASSESSMENT]

  • Overall PR: Risk Assessment: 🟢 LOW
  • No breaking changes to existing APIs
  • Backward compatible via deprecation warning
  • Well-tested with serialization round-trip test

VERDICT:
Worth merging - Clean fix that solves the serialization problem without breaking existing code.

KEY INSIGHT:
Converting a PrivateAttr to a public Field is the right solution here — it makes the flag serializable while the __setattr__ interception maintains backward compatibility for code that still writes to _is_subscription.


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

Comment thread openhands-sdk/openhands/sdk/llm/llm.py Outdated
Comment thread openhands-sdk/openhands/sdk/llm/llm.py Outdated
…ote transport

`is_subscription` was a plain property backed by the `_is_subscription`
PrivateAttr, so it was silently dropped whenever an LLM was serialized — in
particular when a RemoteConversation ships the agent's LLM to an agent-server.
The server rebuilt the LLM with is_subscription == False, and every
subscription-specific behavior stopped applying server-side:

- the streaming on_token exemption in responses()
  (ValueError: Streaming requires an on_token callback),
- the Codex system-prompt transform (transform_for_subscription),
- reasoning-item stripping for store=false (follow-up requests reference
  unresolvable item IDs -> 404 from the Codex endpoint).

Net effect: LLM.subscription_login() worked locally but silently broke with
remote workspaces.

Promote the property to a `@computed_field` (so it is included in model_dump)
and add a wrap model_validator that restores the backing `_is_subscription`
PrivateAttr on validation. The public read API (`llm.is_subscription`) and the
existing `_is_subscription` write path are both unchanged, so this is additive
and not an API break.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xingyaoww xingyaoww force-pushed the fix/serialize-is-subscription branch from 3b118a9 to 2daa3f8 Compare June 10, 2026 15:58
@xingyaoww

Copy link
Copy Markdown
Member Author

Pushed 2daa3f8 reworking the fix after the first attempt tripped the API-breakage gate.

First attempt made is_subscription a plain Field(...). That registered as ATTRIBUTE_CHANGED_VALUE (property → field) in check_sdk_api_breakage.py, which demands a minor-version bump — a real breakage.

Current approach keeps is_subscription a property but decorates it with @computed_field (so it lands in model_dump()), keeps the _is_subscription PrivateAttr and its existing write path untouched, and adds a wrap model_validator to restore the flag on validation. This is additive — no API break.

Verified against the released baseline:

  • Run Python API breakage checksuccess (step 5)
  • Run agent server REST API breakage checksuccess (step 6)
  • Check OpenAPI Schema, sdk-tests, cross-tests, tools-tests, agent-server-tests, pre-commit → all green
  • pytest tests/sdk → 3535 passed; ruff + pyright clean

The red ❌ on Python API / REST API (OpenAPI) is not the breakage check — both of those steps pass. The jobs fail only at the final Post … breakage report to PR step with HttpError: Requires authentication, because the GITHUB_TOKEN is read-only for this PR's author association and can't post the report comment. Neither check is in the branch's required-status set, so this doesn't gate merge; happy to adjust if you'd prefer the report suppressed when the token is read-only.

@xingyaoww xingyaoww requested a review from all-hands-bot June 10, 2026 21:12

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ QA Report: PASS WITH ISSUES

The SDK behavior behind the PR goal works: subscription-mode LLMs now survive serialization/revalidation and retain the streaming exemption after transport-like round trips.

Does this PR achieve its stated goal?

Yes. The PR set out to serialize is_subscription so subscription mode survives remote agent-server transport; I exercised the SDK's LLM and Agent serialization/rebuild path and confirmed main loses the flag while this PR preserves it. I also called LLM.responses() after the round trip with stream=True and no on_token: main fails locally with ValueError: Streaming requires an on_token callback, while the PR keeps is_subscription=True and proceeds to the HTTP layer, proving the subscription streaming exemption remains active after serialization.

Phase Result
Environment Setup make build completed and installed the uv environment successfully.
CI Status ⚠️ Most checks are green, but Validate PR description was failing and qa-changes was still in progress when checked.
Functional Verification ✅ Verified serialization, Agent payload transport shape, JSON wire-style round trip, plain non-subscription behavior, and streaming exemption behavior.
Functional Verification

Test 1: Subscription flag survives SDK/Agent serialization round trip

Step 1 — Reproduce baseline on origin/main (e01557ee):
Ran a script that creates a Codex-style LLM, sets _is_subscription=True as the subscription auth flow does, serializes/revalidates the LLM, then serializes/revalidates an Agent containing that LLM:

baseline_llm_dump_has_is_subscription= False
baseline_llm_dump_value= None
baseline_restored_llm_is_subscription= False
baseline_agent_llm_dump_has_is_subscription= False
baseline_restored_agent_llm_is_subscription= False
baseline_plain_restored_is_subscription= False

This confirms the reported bug exists on main: the subscription flag is absent from serialized LLM/Agent payloads and comes back as False after reconstruction.

Step 2 — Apply the PR's changes:
Used the checked-out PR branch fix/serialize-is-subscription at 2daa3f83.

Step 3 — Re-run the same scenario on the PR:

pr_llm_dump_has_is_subscription= True
pr_llm_dump_value= True
pr_restored_llm_is_subscription= True
pr_agent_llm_dump_has_is_subscription= True
pr_restored_agent_llm_is_subscription= True
pr_plain_restored_is_subscription= False

This shows the fix works for the transport-relevant SDK objects: subscription LLMs serialize with the flag and restore it, while ordinary LLMs still restore as non-subscription.

Test 2: JSON wire-style Agent payload round trip

Ran an Agent payload through json.dumps / json.loads before model validation on the PR:

wire_llm_is_subscription_field= True
wire_restored_agent_llm_is_subscription= True

This confirms the field survives a JSON-shaped payload, matching the kind of boundary used by remote transport rather than only an in-memory Python dict.

Test 3: Streaming on_token exemption remains active after round trip

Step 1 — Reproduce baseline on origin/main:
With stream=True, num_retries=0, a dummy API key, and base_url=http://127.0.0.1:9, I called responses() after the serialization round trip without providing on_token:

baseline_after_roundtrip_is_subscription= False
baseline_responses_exception_type= ValueError
baseline_responses_exception_first_line= Streaming requires an on_token callback

This confirms the user-visible breakage described in the PR: after serialization, the LLM is no longer treated as subscription mode and the local streaming guard rejects the call before any HTTP request.

Step 2 — Re-run on the PR:

pr_after_roundtrip_is_subscription= True
pr_responses_exception_type= LLMServiceUnavailableError
pr_responses_exception_first_line= litellm.InternalServerError: InternalServerError: OpenAIException - [Errno 111] Connection refused

The connection refusal is expected because I intentionally pointed the request at a closed localhost port. The important behavior is that the local on_token error is gone and the call reaches the HTTP layer, which demonstrates the subscription-specific streaming exemption survived the serialization round trip.

Unable to Verify

I did not use real ChatGPT/Codex subscription credentials or run a live Codex request. The functional check intentionally used a dummy key and closed localhost endpoint to verify the SDK transport/streaming behavior without external credentials.

Issues Found

  • 🟡 CI: Validate PR description is currently failing according to gh pr checks; no functional issues were found in the changed SDK behavior.

This QA report was created by an AI agent (OpenHands) on behalf of the user.

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.

2 participants