feat: OpenAI backend init#408
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has required the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Docs preview: https://7648320f.seal-docs.pages.dev |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a complete OpenAI/ChatGPT integration alongside existing Claude/Anthropic support. Users can now run ChangesOpenAI/ChatGPT Backend Integration
🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Here's my review of #408. I found three issues worth fixing before merge — one of them is a real security regression — plus a couple of smaller nits. Blockers1. #[cfg(unix)]
{
seal_utils::io::ensure_dir_with_mode(&path, 0o600).ok();
}
2. Token refresh is dead code. 3. "openai" => seal_utils::env::openai_api_key().is_some(),So with Smaller things
What's goodThe SSE refactor reads cleanly — passing Happy to fix any of the three blockers in a follow-up if you want — just say the word. |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/seal-cli/src/login.rs`:
- Around line 180-201: The PkceCodes struct and the code_challenge value are
unused noise: remove the code_challenge parameter from the function
(exchange_code_for_tokens or the surrounding function that constructs
PkceCodes), delete the code_challenge field from the PkceCodes struct, and stop
constructing/storing code_challenge; keep and send only code_verifier in the
form post to "{issuer}/oauth/token". Update any call sites and type uses that
reference PkceCodes or the now-removed code_challenge parameter to use only the
code_verifier value.
- Around line 59-61: The HTTP client is built without a per-request timeout, so
a single hung request can block the login flow (see client built in this file
and the poll_for_token/MAX_WAIT loop); update the reqwest::Client::builder()
call to set a per-request timeout (e.g., using std::time::Duration and
.timeout(...)) to a value smaller than MAX_WAIT (so each request fails fast and
the poll loop can respect the overall 15-minute budget), and add the necessary
import for Duration; ensure the chosen timeout applies to the token-polling and
token-exchange requests made by poll_for_token.
- Around line 30-41: deser_interval currently always deserializes the field as a
String which fails for numeric JSON; update deser_interval to accept both
numeric and string JSON by attempting to deserialize into an Option<u64> (or a
u64 then wrap) first and falling back to String->parse if needed so that the
struct field interval: Option<u64> and its deserializer (deser_interval) handle
both JSON numbers and numeric strings when deserializing UserCodeResp; reference
the deser_interval function and the interval field to locate where to implement
this dual-path deserialization.
- Around line 234-237: persist_tokens currently writes chatgpt-auth.json using
home_dir().join(".seal") and then mistakenly calls ensure_dir_with_mode on the
full file path (ensure_dir_with_mode(&path, 0o600)) which is a directory helper
and thus never applies file mode; also reads use seal_utils::paths::seal_home()
causing SEAL_HOME inconsistency. Fix by using seal_utils::paths::seal_home() to
compute the directory, call seal_utils::io::ensure_dir_with_mode on that
directory (not the file) to ensure the directory exists, write the file via
seal_utils::io::write to seal_home().join("chatgpt-auth.json"), and on Unix
explicitly set the file's mode to 0o600 after writing (e.g. with
std::fs::set_permissions or libc/nix chmod) rather than calling
ensure_dir_with_mode on the file path; update references in persist_tokens and
any token-read logic to consistently use seal_utils::paths::seal_home().
In `@crates/seal-cli/src/main.rs`:
- Line 314: The "openai" match arm in run_agent currently gates access using
only seal_utils::env::openai_api_key(), which blocks OAuth-only users; update
the "openai" condition in run_agent to allow either an API key OR presence of
OAuth credential files (e.g., ~/.seal/chatgpt-auth.json and ~/.codex/auth.json)
or by delegating to the same credential lookup used by the daemon; modify the
"openai" branch so it checks seal_utils::env::openai_api_key().is_some() ||
oauth_file_exists("~/.seal/chatgpt-auth.json") ||
oauth_file_exists("~/.codex/auth.json") (or call the shared credential lookup
helper) before returning the "No API key found" error.
In `@crates/seal-runtime/src/llm/backends/openai.rs`:
- Around line 1526-1531: The live tool-call test is invoking backend.chat with
an empty tool list, so the model cannot legally produce a ToolUseStart event.
Update the test setup in openai.rs around backend.chat to pass the relevant
ToolDefinition(s) used by this scenario instead of &[], ensuring the request
actually advertises callable tools. Use the existing test harness and
backend.chat call site to locate the change, and keep the system prompt and
stream assertions aligned with the intended tool-use behavior.
- Around line 41-57: fetch_openai_model_field_at currently tries to read
unsupported fields (like max_output_tokens and context_length) from the OpenAI
/v1/models/{model} response; remove that probing by changing
fetch_openai_model_field_at (and the callers fetch_openai_max_output_tokens_at
and fetch_openai_context_length_at if needed) to not call the models endpoint
for those fields and instead return None (or use a maintained internal
model-capacity mapping) so model_info() relies on correct defaults/mapping
rather than always-failing API probes. Ensure you only touch
fetch_openai_model_field_at (and update fetch_openai_max_output_tokens_at /
fetch_openai_context_length_at) and leave other request logic unchanged.
- Around line 239-245: The request body currently sets "store": false and
"include": [] which drops stateless reasoning context; when reasoning is
enabled, add "reasoning.encrypted_content" to the include array (so change the
body construction around the include field), ensure any extracted reasoning
items' encrypted_content from responses are captured, and append those encrypted
reasoning items into the next request's input_items (and into the input array)
so subsequent calls resend the encrypted reasoning state; also update the
include existence checks (the logic around lines testing include at 269–272) to
handle the "reasoning.encrypted_content" entry.
In `@crates/seal-runtime/src/llm/chatgpt_token_refresh.rs`:
- Around line 128-134: The POST to auth.openai.com currently has no per-request
timeout and can hang; add a bounded timeout around the request (either via
reqwest's per-request .timeout(Duration::from_secs(<N>)) on the client.post(...)
builder or by wrapping the .send().await call with
tokio::time::timeout(Duration::from_secs(<N>))). Update the code around
client.post(...).body(body).send().await to enforce this timeout, convert the
timeout error into the existing error flow (map_err with a clear message like
"refresh request timed out") and import std::time::Duration (or tokio::time) and
adjust error mapping so both network and timeout failures return an Err string
consistent with the current pattern.
- Around line 96-103: The refresh flow currently only extracts access_token in
do_refresh and then ensure_fresh_token calls write_refreshed_token with the old
stored.refresh_token; if the OAuth server rotates refresh tokens the new
refresh_token must be persisted. Update do_refresh to parse and return the
rotated refresh_token (e.g., return a struct or tuple with access_token and
optional refresh_token), and modify ensure_fresh_token to call
write_refreshed_token with the new refresh_token when present (fall back to
stored.refresh_token only if the response lacks a rotated token); reference
do_refresh, ensure_fresh_token, write_refreshed_token, and stored.refresh_token
to locate the changes.
In `@crates/seal-runtime/src/llm/mod.rs`:
- Around line 488-491: The debug log prints the full CredentialSource (including
user file paths) which leaks identifiers; change the eprintln! in the llm/mod.rs
snippet to print only the credential source kind instead of the full value.
Locate the eprintln! that references model_config.name and source and replace
the direct interpolation of source with either a short helper (e.g.,
CredentialSource::kind() or a match on source matching
SealChatGptAuthFile/CodexAuthFile/etc.) that returns only the variant/kind
string (e.g., "AuthFile", "Env", "Default"), and log that kind string.
In `@crates/seal-utils/src/credentials.rs`:
- Around line 188-190: In read_openai_credential(), instead of returning
Err(NoCredentialError) when user_env_has("OPENAI_API_KEY") is true, return the
stored value from the user_env; call the corresponding getter (e.g.,
user_env_get("OPENAI_API_KEY") or equivalent) and return that credential
(wrapped in the function's success type) so lookup_openai() can use the
fallback; update the branch that currently does "if
user_env_has(\"OPENAI_API_KEY\") { return Err(NoCredentialError); }" to retrieve
and return the value instead.
In `@docs/site/src/content/docs/provider-backends.mdx`:
- Around line 18-23: The Aside note is broken by an extra blank line causing the
sentence to split and show a dangling "+"; edit the Aside block containing the
text fragments `provider = "openai"` and `OPENAI_API_KEY` (the <Aside>
component) to remove the empty line so the two lines form one continuous
sentence and also delete the stray "+" if still present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b4a339fc-3c98-4346-ba79-8472c21baf19
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (21)
crates/seal-cli/Cargo.tomlcrates/seal-cli/src/lib.rscrates/seal-cli/src/login.rscrates/seal-cli/src/main.rscrates/seal-cli/src/tui/init/input.rscrates/seal-cli/src/tui/init/rendering.rscrates/seal-cli/src/tui/init/state.rscrates/seal-cli/src/tui/init/submit.rscrates/seal-cli/src/tui/init/tests.rscrates/seal-runtime/src/llm/backends/anthropic.rscrates/seal-runtime/src/llm/backends/mod.rscrates/seal-runtime/src/llm/backends/openai.rscrates/seal-runtime/src/llm/backends/openrouter.rscrates/seal-runtime/src/llm/chatgpt_token_refresh.rscrates/seal-runtime/src/llm/mod.rscrates/seal-runtime/src/llm/streaming.rscrates/seal-runtime/src/llm/tests_common.rscrates/seal-utils/src/credentials.rscrates/seal-utils/src/env.rsdocs/site/src/content/docs/install.mdxdocs/site/src/content/docs/provider-backends.mdx
Greptile SummaryThis PR adds a native OpenAI backend targeting the Responses API, a
Confidence Score: 4/5Safe to merge after confirming the reasoning-state assembly order in turn.rs (noted in a prior outside-diff comment) is addressed; all auth/security fixes from the previous review cycle are in place. The critical auth and file-permission issues from earlier rounds are resolved. One outstanding concern flagged in a previous outside-diff comment — the assistant content being assembled as Text → ReasoningState → ToolUse rather than the API's emission order of ReasoningState → Text → ToolUse — means replayed history has items in the wrong order, which can trigger 400s from the Responses API on subsequent turns that include reasoning. That item is unresolved in the current HEAD. crates/seal-agent/src/agent_loop/turn.rs — the reasoning-state ordering fix from the prior review comment has not yet landed. Important Files Changed
Reviews (14): Last reviewed commit: "test: add OPENAI_API_KEY to workflows an..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
wit/seal.wit (1)
1-1:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBump the WIT package version for this ABI change.
Adding new cases to exported variants changes the component contract. Keeping
seal:agent@0.3.0makes old generated bindings and mixed host/guest builds look compatible when they are not.Suggested fix
-package seal:agent@0.3.0; +package seal:agent@0.4.0;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@wit/seal.wit` at line 1, The package version in the WIT file still reads "package seal:agent@0.3.0" despite ABI-breaking changes (new variant cases); update the package declaration to a new patch/minor version (e.g., "package seal:agent@0.4.0" or appropriate semver bump) so generated bindings and host/guest mixes are flagged as incompatible; modify the line with the package declaration in seal.wit to the bumped version.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/seal-cli/src/login.rs`:
- Around line 156-167: The poll_for_token loop currently only retries for
specific HTTP statuses and bails on other responses; update poll_for_token to
parse the response body (resp.text().await or preferably resp.json()) and handle
RFC 8628 errors: if the returned error field is "authorization_pending" then
continue polling as before, if it's "slow_down" increase the interval (e.g.,
interval = interval.saturating_add(5) or add 5 seconds) for subsequent sleeps,
and only bail for other error values; make sure to still enforce the MAX_WAIT
via start.elapsed() and to apply the adjusted interval when computing sleep (the
variables to update are poll_for_token, resp/body parsing, interval, start, and
the sleep call).
In `@crates/seal-cli/src/main.rs`:
- Around line 152-158: The login branch unnecessarily creates a nested tokio
Runtime and uses block_on; since main is already annotated with #[tokio::main],
remove the Runtime::new() and rt.block_on(...) calls and instead directly await
the async login function (call seal_cli::login::run_device_code_login().await).
Update the match arm for LoginProvider::OpenAi to call the async function with
.await and remove the surrounding runtime allocation so spawned tasks use the
existing runtime.
In `@crates/seal-cli/src/tui/init/rendering.rs`:
- Around line 128-130: The example model IDs shown for the OpenAI provider are
inconsistent with the canonical default model; update the string returned in the
match arm for ProviderChoice::ChatGptSubscription | ProviderChoice::OpenAiApiKey
to use "gpt-5.4, gpt-4o-mini" (or otherwise indicate the example is not the
default) so the UI example aligns with the canonical default model used
elsewhere (e.g., where gpt-5.4 is expected).
In `@wit/seal.wit`:
- Around line 623-624: The opaque variant reasoning-state(string) must carry
provider provenance so backends can detect foreign state; change the definition
of reasoning-state to include provider and schema metadata (e.g., replace or
extend reasoning-state(string) with a structured variant containing {payload:
string, provider: string, provider_version?: string, schema?: string} or add an
accompanying provider field) or alternatively document and implement an explicit
"strip-on-provider-change" rule before accepting reasoning-state into generic
history; update any uses/serializers/deserializers that reference
reasoning-state (and the similar variant around lines referenced) to read/write
the new metadata so replay can be gated by provider/schema checks.
---
Outside diff comments:
In `@wit/seal.wit`:
- Line 1: The package version in the WIT file still reads "package
seal:agent@0.3.0" despite ABI-breaking changes (new variant cases); update the
package declaration to a new patch/minor version (e.g., "package
seal:agent@0.4.0" or appropriate semver bump) so generated bindings and
host/guest mixes are flagged as incompatible; modify the line with the package
declaration in seal.wit to the bumped version.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 40642b28-0ac3-47c3-b3f9-d0aa81ce0249
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
crates/seal-agent/src/agent_loop/compact.rscrates/seal-agent/src/agent_loop/history.rscrates/seal-agent/src/agent_loop/turn.rscrates/seal-agent/src/agent_loop/wit_convert.rscrates/seal-agent/src/host.rscrates/seal-agent/src/messages.rscrates/seal-cli/Cargo.tomlcrates/seal-cli/src/lib.rscrates/seal-cli/src/login.rscrates/seal-cli/src/main.rscrates/seal-cli/src/tui/init/input.rscrates/seal-cli/src/tui/init/rendering.rscrates/seal-cli/src/tui/init/state.rscrates/seal-cli/src/tui/init/submit.rscrates/seal-cli/src/tui/init/tests.rscrates/seal-policy/src/grant.rscrates/seal-policy/src/manifest.rscrates/seal-policy/src/manifest/tests/basic_parsing.rscrates/seal-policy/src/manifest/tests/thinking_config.rscrates/seal-runtime/src/engine/conversions.rscrates/seal-runtime/src/engine/stream_chat.rscrates/seal-runtime/src/grant_store.rscrates/seal-runtime/src/llm/backends/anthropic.rscrates/seal-runtime/src/llm/backends/mock.rscrates/seal-runtime/src/llm/backends/mod.rscrates/seal-runtime/src/llm/backends/openai.rscrates/seal-runtime/src/llm/backends/openrouter.rscrates/seal-runtime/src/llm/chatgpt_token_refresh.rscrates/seal-runtime/src/llm/mod.rscrates/seal-runtime/src/llm/streaming.rscrates/seal-runtime/src/llm/tests_common.rscrates/seal-utils/src/credentials.rscrates/seal-utils/src/env.rsdocs/ARCHITECTURE.mddocs/MANIFEST.mddocs/site/src/content/docs/install.mdxdocs/site/src/content/docs/provider-backends.mdxdocs/site/src/content/docs/reference/manifest/index.mdxdocs/site/src/content/docs/reference/manifest/model.mdxschemas/seal.toml.jsonwit/seal.wit
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
crates/seal-runtime/src/llm/backends/openai.rs (1)
82-119:⚠️ Potential issue | 🟠 MajorThis reintroduces the unsupported
/v1/models/{id}capability probe.The official Models API describes the model object as basic metadata (
id,created,object,owned_by); it does not exposemax_output_tokensorcontext_length. As written, these helpers will keep falling back for unknown models, and the fetch/live tests below are pinning the same undocumented schema instead of a real API contract. (platform.openai.com)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/seal-runtime/src/llm/backends/openai.rs` around lines 82 - 119, The helpers fetch_openai_max_output_tokens_at, fetch_openai_context_length_at and fetch_openai_model_field_at are probing the unsupported /v1/models/{id} response for undocumented fields (max_output_tokens, context_length); remove that behavior by stopping calls to fetch_openai_model_field_at and either return None (or a safe default) from fetch_openai_max_output_tokens_at and fetch_openai_context_length_at, and delete or noop fetch_openai_model_field_at; update any callers to treat None as “unknown” rather than relying on these undocumented fields so the code only uses the official Models API surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/seal-policy/src/manifest.rs`:
- Around line 115-117: Manifest currently accepts context_length = 0 which
defers errors to runtime; update Manifest::load to validate that the parsed
model.context_length (the struct field context_length: Option<u64>) if Some(v)
must be > 0 and otherwise return a clear manifest load error. Locate
Manifest::load and after parsing/reading the model entry, check the
context_length Option and return an error (with a helpful message) when value ==
0; leave None unchanged so backend resolution still works. Ensure the error
type/message matches existing Manifest load error conventions so callers can
handle it consistently.
In `@crates/seal-runtime/src/llm/backends/openai.rs`:
- Around line 253-258: refresh_chatgpt_token_if_needed updates self.auth_token
but the model-cap probes still clone self.config.api_key, causing probes (e.g.,
the /v1/models requests in the effective_* probe functions) to use an expired
key; update those probe paths to use the refreshed bearer token by calling the
runtime accessor (current_auth_token() or the auth token field) instead of
self.config.api_key so both chat requests and model-info probes use the same
fresh token (look for functions named refresh_chatgpt_token_if_needed, any
effective_* probe helpers, and model_info()/model probe code and replace cloning
of self.config.api_key with current_auth_token() or equivalent).
- Around line 135-145: messages_to_input_items in openai.rs currently drops
OpenAI's assistant "phase" because the Message/ContentBlock model in llm/mod.rs
has only role + content; to fix, either (a) extend the Message and ContentBlock
types in llm/mod.rs to include an optional phase field (and update
constructors/consumers) so messages_to_input_items can round‑trip phase, or (b)
embed the phase into the serialized content (e.g., an additional metadata field
in the JSON item created by messages_to_input_items) and update any
deserialization paths to read that metadata; update references to Message,
ContentBlock, and messages_to_input_items accordingly and add/adjust tests in
openai.rs to assert phase preservation.
- Around line 586-606: The SSE parser currently only splits frames on "\n\n" and
overwrites event_data for each "data: " line, which breaks CRLF-framed events
and multiline data fields; update the loop that processes buffer to detect both
CRLF and LF frame boundaries (e.g., search for "\r\n\r\n" first then "\n\n") and
when parsing each frame (the code that iterates frame.lines()) accumulate data
lines instead of assigning—append each data fragment (preserving a single "\n"
between fragments per the SSE spec) to event_data and trim only the "data:"
prefix (allow optional leading space), and keep existing logic for event_type
(event_type variable) and JSON parse after assembling the full event_data.
---
Duplicate comments:
In `@crates/seal-runtime/src/llm/backends/openai.rs`:
- Around line 82-119: The helpers fetch_openai_max_output_tokens_at,
fetch_openai_context_length_at and fetch_openai_model_field_at are probing the
unsupported /v1/models/{id} response for undocumented fields (max_output_tokens,
context_length); remove that behavior by stopping calls to
fetch_openai_model_field_at and either return None (or a safe default) from
fetch_openai_max_output_tokens_at and fetch_openai_context_length_at, and delete
or noop fetch_openai_model_field_at; update any callers to treat None as
“unknown” rather than relying on these undocumented fields so the code only uses
the official Models API surface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 373ba33f-e63b-4f38-9d17-faf66fc9edcd
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
crates/seal-agent/src/agent_loop/compact.rscrates/seal-agent/src/agent_loop/history.rscrates/seal-agent/src/agent_loop/turn.rscrates/seal-agent/src/agent_loop/wit_convert.rscrates/seal-agent/src/host.rscrates/seal-agent/src/messages.rscrates/seal-cli/Cargo.tomlcrates/seal-cli/src/lib.rscrates/seal-cli/src/login.rscrates/seal-cli/src/main.rscrates/seal-policy/src/grant.rscrates/seal-policy/src/manifest.rscrates/seal-policy/src/manifest/tests/basic_parsing.rscrates/seal-policy/src/manifest/tests/thinking_config.rscrates/seal-runtime/src/engine/conversions.rscrates/seal-runtime/src/engine/stream_chat.rscrates/seal-runtime/src/grant_store.rscrates/seal-runtime/src/llm/backends/anthropic.rscrates/seal-runtime/src/llm/backends/mock.rscrates/seal-runtime/src/llm/backends/mod.rscrates/seal-runtime/src/llm/backends/openai.rscrates/seal-runtime/src/llm/backends/openrouter.rscrates/seal-runtime/src/llm/chatgpt_token_refresh.rscrates/seal-runtime/src/llm/mod.rscrates/seal-runtime/src/llm/streaming.rscrates/seal-runtime/src/llm/tests_common.rscrates/seal-tui/src/wizard/input.rscrates/seal-tui/src/wizard/rendering.rscrates/seal-tui/src/wizard/state.rscrates/seal-tui/src/wizard/submit.rscrates/seal-tui/src/wizard/tests.rscrates/seal-utils/src/credentials.rscrates/seal-utils/src/env.rsdocs/ARCHITECTURE.mddocs/MANIFEST.mddocs/site/src/content/docs/install.mdxdocs/site/src/content/docs/provider-backends.mdxdocs/site/src/content/docs/reference/manifest/index.mdxdocs/site/src/content/docs/reference/manifest/model.mdxschemas/seal.toml.jsonwit/seal.wit
…s being sent to non-openai providers


Pull request
Summary
Adds a native OpenAI backend (
provider = "openai") that connects directly to the OpenAI Responses API, supporting GPT and o-series models with native reasoning viareasoning_effort. Also adds aseal login openaidevice-code OAuth flow for ChatGPT/Codex subscription users, with automatic token refresh and credential detection from both~/.seal/chatgpt-auth.jsonand~/.codex/auth.json.Related issues
SEA-515
Changes
crates/seal-runtime/src/llm/backends/openai.rsimplementingLlmBackendfor OpenAI's Responses API with SSE streaming, atomic function call delivery viaresponse.output_item.done, o-seriesreasoning_effortsupport, and a subscription routing path tochatgpt.com/backend-api/codexcrates/seal-cli/src/login.rsimplementing the OpenAI device-code OAuth flow (seal login openai), persisting tokens to~/.seal/chatgpt-auth.jsonwith account ID extracted from the id_token JWTcrates/seal-runtime/src/llm/chatgpt_token_refresh.rsfor proactive JWT expiry detection and single-flight token refresh viaPOST https://auth.openai.com/oauth/tokenllm_request_streamingandllm_request_streaming_asyncto accept aparse_ssecallback, decoupling the Anthropic-specific SSE parser from the shared request loopparse_sse_streaming→parse_sse_streaming_anthropicand makes itpubso both the Anthropic and OpenRouter backends can pass it as the callbacklookup_openai(),read_openai_credential(), andopenai_subscription_available()toseal-utils/credentials, with priority ordering: seal chatgpt-auth.json → Codex auth.json →OPENAI_API_KEYenvChatGptSubscriptionandOpenAiApiKeyprovider choices to the init wizard TUI, includingAwaitingChatGptLoginstate withr-to-retry andEsc-to-back key handlingprovider = "openai"inbuild_llm_from_manifest, addsaccount_idanduses_subscriptionfields toBackendConfig, and addsopenai_api_key()toseal-utilsTest plan
build_bodyshape (input items, instructions, streaming flag,max_output_tokensomission for subscription),reasoning_effortpresence/absence acrossThinkingModevariants, tool serialization,map_openai_response_statusmapping,effective_max_tokens/effective_context_lengthclamping, caching, and user-override behavior viawiremockoutput_item.donemessage fallback when no deltas arrived, deduplication when deltas were already received, function call emission withtool_usestop reason,response.failedsurfaced asLlmError::Api, and cleanend_turncompletionOPENAI_API_KEYenv, and env fallback when no auth file exists#[ignore = "live"]) covermodel_infoand a basic chat turn againstapi.openai.com, and subscription chat/tool execution againstchatgpt.com/backend-api/codexparse_sse_streaming_anthropiccallback explicitly and continue to passNotes for reviewers
The
parse_ssecallback isClone + Send + Sync + 'staticto satisfy the retry loop cloning it across attempts. The OpenAI SSE parser mirrors the Anthropic parser's heartbeat, inter-chunk timeout,retry_eligible, andevents_pushedlogic so retry/resume behavior is consistent across backends. The ChatGPT subscription path omitsmax_output_tokensfrom the request body and addsOAI-Product-Sku: codexandChatGPT-Account-IDheaders.StreamOutcomeis nowpubto allow the OpenAI parser to return it from outside thestreamingmodule. Codex auth files are read-only — if a Codex token expires, the user is directed to runseal login openaito create a fresh seal-managed token.Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.