Skip to content

feat(oauth): add Trae SOLO provider - #2964

Merged
diegosouzapw merged 3 commits into
cheaper-inference:mainfrom
S0yora:feat/trae-solo-provider
May 31, 2026
Merged

diegosouzapw merged 3 commits into
cheaper-inference:mainfrom
S0yora:feat/trae-solo-provider

Conversation

@S0yora

@S0yora S0yora commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a fully working Trae SOLO provider (ByteDance's remote agent at solo.trae.ai), replacing the previous placeholder that only registered the provider name without a way to actually route requests.

  • Streaming executor (open-sse/executors/trae.ts) against Trae's solo_agent_remote API — creates a chat session (POST /chat_sessions), consumes the GET /events SSE stream, and translates it into standard OpenAI chat.completions (streaming + non-streaming), with token-usage mapping.
  • Two ways to connect: a browser popup that authorizes via trae.ai and captures the token automatically (with a refresh token), or pasting the Cloud-IDE-JWT token manually. Both persist an encrypted connection.
  • Session modes exposed as model ids: trae/work runs the fast work-mode agent; trae/auto and the named models (e.g. kimi-k2.5, gpt-5.4, gemini-3.1-pro) run in code mode.
  • Dashboard auth modal + provider brand icon (via @lobehub/icons).

Related Issues

  • Related to [BUG] Internal Server Error on Trae #2658 — that issue reported Trae returning an internal error and was closed because Trae exposes no public API, so only a stub provider (no executor, no routing) had shipped as a placeholder. This PR implements the integration for real by talking to the SOLO remote-agent endpoints directly, so requests actually complete instead of erroring out.

Validation

  • npm run lint clean (0 warnings)
  • npm run typecheck:core clean

Tests Added Or Updated

  • tests/unit/trae-executor.test.ts (new) — buildHeaders, streaming and non-streaming responses, plan_item thought accumulation, usage mapping, upstream-error → 502, session-create failure, refreshCredentials (ExchangeToken happy path / no-token / invalid-refresh / token preservation), work vs code mode selection, and the /authorize callback parser.
  • tests/unit/oauth-trae.test.ts (updated) — provider shape, TRAE_CONFIG fields, and mapTokens updated for the SOLO identity fields and ~14-day token lifetime.

Coverage Notes

New production code lives in open-sse/ (executor) and src/ (oauth provider, import route, /authorize callback parser, dashboard modal). The executor and the pure callback parser are unit-tested directly; the HTTP route around the parser is a thin wrapper (DB persist + HTML shell) exercised during the live OAuth flow. No touched file should see a coverage regression.

Reviewer Notes

  • Reverse-engineered, unofficial API. Trae publishes no public API; endpoints/headers were derived from the solo.trae.ai web client and may drift. Auth uses the Authorization: Cloud-IDE-JWT <jwt> scheme.
  • Token lifetime & refresh depend on how you connect:
    • Browser OAuth (popup): captures a long-lived refresh token, so the ~14-day access token (Cloud-IDE-JWT) auto-refreshes headlessly via Trae's ExchangeToken endpoint. The connection keeps working until the refresh token itself expires (months) — then re-authorize via the popup.
    • Manual token paste: no refresh token, so no auto-refresh — the pasted token lasts ~14 days and must be re-pasted on expiry.
  • /authorize lives at the app root, not under /api/oauth/trae/ — Trae's authorize server only accepts a callback URL whose host is loopback 127.0.0.1 and whose path ends in exactly /authorize; anything else short-circuits to "Login Failed". CSRF is handled via the login_trace_id echoed back and verified by the modal.
  • Models are static in the registry — there is no unauthenticated /models endpoint to enumerate them, and auto / work always work regardless.
  • No migrations, no feature flags. The provider already existed in OAUTH_PROVIDERS, so it surfaces in the dashboard automatically.

S0yora added 2 commits May 31, 2026 04:38
Full SOLO remote-agent integration against solo.trae.ai's reverse-engineered
API (upgrades the previous import_token stub).

- open-sse/executors/trae.ts: streaming executor for the solo_agent_remote API
  (POST /chat_sessions + GET /events SSE -> OpenAI chat.completions), accumulating
  plan_item thoughts and mapping token_usage. Headless Cloud-IDE-JWT refresh via
  the ExchangeToken endpoint.
- Session mode via model id: `trae/work` runs the fast work-mode auto agent;
  `trae/auto` and named models (gpt-5.4, kimi-k2.5, gemini-3.1-pro, ...) run in
  code mode.
- Provider registry entry (models + 272k context) and executor wiring.
- Two credential paths: browser /authorize loopback callback (captures the JWT +
  long-lived refresh token) and manual Cloud-IDE-JWT paste via
  POST /api/oauth/trae/import (Zod-validated).
- TraeAuthModal dashboard UI wired into the provider detail page.
- mapTokens/providerSpecificData carry the SOLO common_params identity fields.
- Tests: executor (stream/non-stream/error/refresh/work-mode + callback parser)
  and updated oauth-trae provider tests.
@S0yora
S0yora requested a review from diegosouzapw as a code owner May 31, 2026 02:42

@gemini-code-assist gemini-code-assist 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

This pull request adds comprehensive support for the Trae SOLO provider, introducing the TraeExecutor for API interactions, a dedicated token import route, a loopback callback handler for the desktop OAuth flow, and the TraeAuthModal frontend component. The code review identifies several critical improvements: resolving potential resource leaks by cancelling the stream reader in TraeExecutor, addressing security vulnerabilities by validating origins in postMessage and message event listeners, and handling already-aborted signals and mixed-type message content arrays more defensively. Additionally, the reviewer points out a style guide violation, noting that the loose diagnostic and smoke test scripts must be moved to a subfolder such as scripts/ad-hoc/.

Comment thread open-sse/executors/trae.ts Outdated
} catch {
data = { _raw: payload };
}
if (onEvent(ev, data)) return; // consumer signalled completion

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.

high

When onEvent returns true (signalling completion), the function returns immediately, but the active stream reader is never cancelled. This can leave the underlying connection open and cause resource leaks. We should explicitly cancel the reader before returning.

            if (onEvent(ev, data)) {
              await reader.cancel().catch(() => {});
              return;
            }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 6ff3238 — the SSE reader is now cancelled before returning. Thanks!

Comment thread src/app/authorize/route.ts Outdated
<h2 style="margin:0 0 8px">Trae authorization ${message.success ? "✓" : "failed"}</h2>
<p>${message.success ? "You can close this window." : "Return to the dashboard."}</p>
<script>
try { if (window.opener) window.opener.postMessage(${safe}, "*"); } catch (e) {}

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.

security-high high

Using "*" as the target origin in postMessage is a security vulnerability (CWE-359) because any origin can intercept the message if the popup is opened or hijacked by a malicious site. Since the opener is on the same origin, we should restrict the target origin to window.location.origin.

Suggested change
try { if (window.opener) window.opener.postMessage(${safe}, "*"); } catch (e) {}
try { if (window.opener) window.opener.postMessage(${safe}, window.location.origin); } catch (e) {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The popup callback intentionally runs on 127.0.0.1 — Trae's authorize server rejects any non-loopback auth_callback_url — while the dashboard usually runs on localhost. They're different origins by design, so pinning to window.location.origin silently drops the postMessage and the modal never closes. Fixed in 6ff3238: it now posts to the known loopback origin pair (localhost + 127.0.0.1 on the same port) instead of a single origin — no wildcard, and the random loginTraceId stays as the CSRF guard.

Comment thread src/shared/components/TraeAuthModal.tsx Outdated
Comment on lines +113 to +114
if (!m || m.type !== "trae-oauth-callback") return;
if (traceIdRef.current && m.loginTraceId && m.loginTraceId !== traceIdRef.current) return;

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.

security-high high

This event listener has two security issues:

  1. It lacks origin validation, allowing any external site/iframe to post messages to this window and potentially spoof successful authorization.
  2. The state/traceId validation is bypassed if traceIdRef.current is null (e.g., before the authorization flow is initiated), which could allow session fixation or pre-auth spoofing.

We should validate the origin and strictly enforce that traceIdRef.current is set and matches the incoming loginTraceId.

Suggested change
if (!m || m.type !== "trae-oauth-callback") return;
if (traceIdRef.current && m.loginTraceId && m.loginTraceId !== traceIdRef.current) return;
if (ev.origin !== window.location.origin) return;
if (!m || m.type !== "trae-oauth-callback") return;
if (!traceIdRef.current || m.loginTraceId !== traceIdRef.current) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same loopback nuance: the dashboard (localhost) and the popup callback (127.0.0.1) are different origins by design, so ev.origin !== window.location.origin would reject the legitimate message. Fixed in 6ff3238 — the listener now accepts that known loopback origin pair and strictly requires a matching loginTraceId (kept your strict-traceId tightening — good catch).

@@ -0,0 +1,77 @@
// Trae SOLO auth diagnostic — probes the live API with your token across several

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.

medium

According to the repository style guide (Section 1), all maintenance, debugging, generation, or experimental scripts must be placed strictly inside one of the scripts/ subfolders (e.g., scripts/ad-hoc/ for one-shot/experimental scripts). Loose scripts should never be placed in the top-level scripts/ folder. Please move this script to scripts/ad-hoc/diag-trae-auth.mjs.

References
  1. ALL maintenance, debugging, generation, or experimental scripts MUST be placed strictly inside one of the scripts/ subfolders. NEVER dump loose scripts in the top-level scripts/ folder. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6ff3238 — moved to scripts/ad-hoc/

Comment thread scripts/smoke-trae.mjs Outdated
@@ -0,0 +1,61 @@
// End-to-end smoke test: dergaет TraeExecutor против реального Trae с твоим

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.

medium

According to the repository style guide (Section 1), all maintenance, debugging, generation, or experimental scripts must be placed strictly inside one of the scripts/ subfolders (e.g., scripts/ad-hoc/ for one-shot/experimental scripts). Loose scripts should never be placed in the top-level scripts/ folder. Please move this script to scripts/ad-hoc/smoke-trae.mjs.

References
  1. ALL maintenance, debugging, generation, or experimental scripts MUST be placed strictly inside one of the scripts/ subfolders. NEVER dump loose scripts in the top-level scripts/ folder. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6ff3238 — moved to scripts/ad-hoc/

Comment on lines +33 to +37
else if (Array.isArray(m.content)) {
content = m.content
.map((p) => (p && typeof p === "object" ? String((p as JsonRecord).text ?? "") : ""))
.join("");
}

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.

medium

In flattenQuery, if m.content is an array, the current mapping assumes all elements are objects with a text property. However, in some OpenAI-compatible payloads, elements of the content array can be plain strings. We should handle string elements defensively to avoid mapping them to empty strings.

    else if (Array.isArray(m.content)) {
      content = m.content
        .map((p) => {
          if (typeof p === "string") return p;
          if (p && typeof p === "object") return String((p as JsonRecord).text ?? "");
          return "";
        })
        .join("");
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 6ff3238 — array content elements that are plain strings are now handled.

Comment on lines +167 to +170
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(new Error("trae stream timeout")), STREAM_TIMEOUT_MS);
const onAbort = () => ctrl.abort();
if (signal) signal.addEventListener("abort", onAbort, { once: true });

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.

medium

If the passed signal is already aborted when streamEvents is called, the abort event listener will not be triggered. We should check signal?.aborted upfront and abort the controller immediately to prevent unnecessary network requests.

    const ctrl = new AbortController();
    if (signal?.aborted) {
      ctrl.abort();
    }
    const timer = setTimeout(() => ctrl.abort(new Error("trae stream timeout")), STREAM_TIMEOUT_MS);
    const onAbort = () => ctrl.abort();
    if (signal) signal.addEventListener("abort", onAbort, { once: true });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 6ff3238 — aborts upfront when the caller's signal is already aborted.

…anup, scripts)

- Restrict the /authorize postMessage and the modal listener to the loopback
  origin pair (localhost + 127.0.0.1) instead of "*"/single-origin. The dashboard
  runs on localhost while Trae forces the callback onto 127.0.0.1, so a single
  window.location.origin target silently dropped the success message (popup never
  closed). Addresses CWE-359 without breaking the cross-loopback flow.
- TraeExecutor: cancel the SSE reader on completion, abort upfront when the caller
  signal is already aborted, and accept string elements in array message content.
- Move ad-hoc dev scripts to scripts/ad-hoc/ per the repo style guide.
@diegosouzapw
diegosouzapw merged commit c4a359e into cheaper-inference:main May 31, 2026
3 checks passed
diegosouzapw added a commit that referenced this pull request May 31, 2026
feat(oauth): add Trae SOLO provider
@diegosouzapw diegosouzapw mentioned this pull request Jun 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…provider

feat(oauth): add Trae SOLO provider
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…provider

feat(oauth): add Trae SOLO provider
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…provider

feat(oauth): add Trae SOLO provider
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…provider

feat(oauth): add Trae SOLO provider
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…provider

feat(oauth): add Trae SOLO provider
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…provider

feat(oauth): add Trae SOLO provider
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