Skip to content

feat: add Trae model integration - #5503

Closed
chihumyum wants to merge 4 commits into
apache:mainfrom
chihumyum:feat/trae-provider
Closed

chihumyum wants to merge 4 commits into
apache:mainfrom
chihumyum:feat/trae-provider

Conversation

@chihumyum

@chihumyum chihumyum commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Trae as a model provider: ByteDance employee SSO plus Trae public accounts (CN and SG, IDE and SOLO). Public accounts sign in through a loopback PKCE flow in the system browser, load their account-scoped model directory, and run inference through Trae's native llm_utils_chat route. The connection catalog and the OAuth protocol carry the account variant so a connection remembers which region and product it belongs to.

Findings from verification with real accounts that shaped the approach:

  • Trae's authorization page accepts only http://127.0.0.1:<port>/authorize as the callback, so the response is bound by the login trace id it echoes back rather than a random path.
  • International accounts exchange the code on the global issuers in order (growsg-normal, then grow-normal); the issuer that signed is recorded on the identity so refresh returns to it. The first issuer's verdict is the one reported, and a 401/403 stops the loop: the second issuer only ever sees a code the first one already consumed.
  • Trae counts every machine/device pair against the account's device limit. The pair is derived per Runtime Host root (keyed HMAC, the root id never leaves the machine), so every Trae connection and re-login of one install is a single device instead of spending a slot per sign-in.
  • Tool schemas are projected onto the subset Gemini-backed routes accept (Trae forwards them as function_declarations and rejected the rest as stream error 4027).
  • Stream errors keep the provider's message; 1005 (model outside the plan) and 4008 (quota) classify as billing, not credentials.
  • US accounts are refused at login: their TTP gateway exposes only the IDE's create_agent_task protocol, not llm_utils_chat.
  • Desktop refused an open_external presentation without a stateHint, which loopback flows never carry; the hint is optional on the wire and now optional in Desktop.

The Runtime Host compatibility epoch moves to 170: catalog page headers and OAuth create targets carry traeAccount, and live Turn snapshots carry the provider queue position.

Not in this PR: the inline Standard/Max switch and the model load percentage that an earlier draft showed in the old dropdown picker. The Selector picker on main has no open-state hook, so Standard and Max list as two rows and the load percentage is stored but not displayed (see docs/trae-provider.md).

Verification

Real accounts (2026-09-19): CN and SG, IDE and SOLO, all four combinations signed in, synced their model directories and completed single-turn inference with tool definitions on both Gemini- and GPT-backed routes. Employee SSO was verified earlier on the ByteDance intranet; from outside it ends as authorization_failed (outcome_unknown) by design.

Local checks after rebasing onto 205a06efb (head e957d1a47):

  • Clean build, lint, formatting, typecheck, Desktop/UI Knip: pass, using Node 24.19.0 and npm 11.19.0.
  • Renderer architecture against the exact base with --strict-base, protocol epoch (169 to 170; 17 guard tests), locale hygiene, ASF headers, Windows inventory, and Astryx 0.6.2 inventory: pass.
  • Full Core: 888 pass. Runtime: 3567 pass / 14 skipped. Desktop: 2783 pass. UI: 558 pass. Runtime Host: 2002 pass / 12 skipped.
  • npm run check:release: pass, including all 203 release contract tests.

The compatibility-epoch conflict is resolved by retaining main's epoch-169 not_admitted message-query outcome and assigning epoch 170 to the Trae protocol additions.

The three P2 findings remain fixed: tool fragments cannot rename an existing call, truncated streams carry a retryable structured failure, and employee device codes appear in enrollment and re-login. Windows recovery includes both Trae authorization sources in its path filter.

The rebase preserves main's process timing, removed Regenerate action, and per-model thinking defaults. Two integration regressions are covered: Trae Standard/Max creation must receive the configured thinking default from its variant inventory, and a queued Trae request must suppress the process-header working cue. Both failures were reproduced before the fixes. Trae account selection and its mark remain in features/connection-settings without growing legacy renderer debt; the vendored Lobe Icons SVG retains its source, digest, and MIT notice.

Contract tests cover the loopback PKCE handoff and callback binding, issuer order and the device-limit verdict, derived device identity, token refresh and credential round trips, region isolation, catalog merging, native tool round trips, Gemini schema projection, provider queue events, and the protocol decoders for the account variant.

Not verified with a real account: multi-turn tool-call round trips and long sessions (covered by contract tests only).

Review focus

  • packages/runtime-host/src/protocol/{oauth,runtime-policy,index}.ts: the create-target union (Codex slug/name vs. Trae account) and the epoch bump.
  • packages/runtime/src/trae/public-protocol.ts traePublicDeviceIdentity: the identity Trae sees is an HMAC of the Host root id, stable per install.
  • packages/runtime/src/trae/public-authorization.ts TraeTokenEndpointError: keeps the issuer's error code and a bounded, redacted message on top of the category; never the response body.
  • packages/runtime/src/trae/messages.ts traeToolSchema: the Gemini schema subset projection.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex assisted with rebasing, conflict resolution, renderer boundary fixes, third-party icon attribution, the P2 review fixes, adaptation to main's thinking defaults and process timing, and regression validation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XXL Over 2500 readable lines label Sep 19, 2026

@Astro-Han Astro-Han 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.

Exact-head review: integration blocked

Reviewed commit 545bf091cd033fc49aa9ff79c5492dae7887cd38. The reviewed Trae OAuth/Host, credential identity, token lifecycle, renderer boundary, model discovery/routing, failure/cancellation, persistence, and regression-test paths did not yield an independently confirmed P0–P3 correctness finding.

The PR is not merge-ready at this head:

  • Required test is failing (run 35429289105, job 105860844339). The failure is modified-file locale hygiene in apps/desktop/src/renderer/settings/provider-oauth-section.tsx:215,248: two new locale.startsWith('zh') branches must be moved into the UiCatalog.
  • The PR is CONFLICTING / DIRTY against current main. The merge-tree has content conflicts in packages/runtime-host/src/protocol/index.ts, packages/ui/src/chat-turn.tsx, and packages/ui/src/conversation-copy.ts.

audit and label are successful, and git diff --check is clean. The complete evidence report is recorded in reports/pr5503-545bf091-review.md.

Automated review notice: This review was generated by an AI agent and does not replace independent human review.

@Astro-Han Astro-Han 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.

Reviewed across all layers (runtime/trae, host protocol, desktop). The architecture holds up well: the callback binding is sound — login_trace_id is client-generated (randomUUID()), echoed through the authorize URL, verified at the loopback, single-use, and the code exchange is PKCE-S256-bound, so it functions as OAuth state; the multi-issuer fallback ordering is correct (non-401/403 falls through, invalid_token breaks, first verdict reported); device identity is correctly derived because the HMAC input (storage rootId) never leaves the machine; region isolation is enforced by origin pinning rather than trust in the caller; epoch 167 rejects mismatched peers at handshake; and the queue-position field is purely informational with proper lifecycle clearing.

Three P2s inline. Consolidated P3s below — none block.

P3 — traeAccount employee double-spelling: traeAccountFields accepts both omitted and 'employee' verbatim, but sameOAuthLoginTarget/sameTarget compare with ===, so a replayed attempt with the other spelling gets invalid_request/attempt_conflict instead of the idempotent projection (and catalog names diverge: Trae vs Trae · ByteDance SSO). Canonicalize 'employee' → omitted at decode so there's one spelling.

P3 — absent region echo skips US/region-mismatch enforcement (public-authorization.ts): actualRegion is only enforced when present; a callback omitting userRegion/userTag proceeds to exchange — a US account could end up with a stored credential that fails at inference (US gateway lacks llm_utils_chat), defeating the documented refusal.

P3 — DesktopOAuthLoginTarget in bridge-contract.d.ts doesn't declare traeAccount: the renderer port and runtime both carry it fine; the .d.ts just lags the wire shape.

P3 — refresh_failed has no copy mapping: the trae model-sync failure returns reason: 'refresh_failed' with specific guidance text, but settings-provider-copy.ts has no mapping for it and the text contains no classifiable keywords, so users get the generic retry message instead of "open the connection and refresh its model list".

P3 — provider-controlled inputs worth hardening: unguarded new URL(rawHost) on LoginHost throws TypeError → internal_failure instead of invalid_response; the legacy userJwt path assumes JSON — if the older CN wire sends a raw JWT string it can never complete (no test coverage; worth checking against the trae-api-proxy reference); employee decodeTokens has no expiry ceiling (public caps at 366d); a well-formed but non-allowlisted LoginHost aborts login rather than falling back to the next guidance origin.

P3 — SSE/schema edges: [DONE] with trailing whitespace throws; an empty data: payload on a known event → JSON.parse('') → dead turn classified unknown; schema projection silently widens (multi-element allOf dropped, tuple items collapse to items[0], patternProperties/not/if-then-else dropped) — the tool-side arg check is the backstop; recursion depth is guarded only for $ref/allOf, not properties/items/anyOf — a deeply-nested hostile tool schema → RangeError.

P3 — type shape: traeAccount is type-level exposed on non-trae create targets (runtime decode rejects; cosmetic only).

The PR notes multi-turn tool-call round-trips and long sessions weren't verified against real accounts — P2-1 lives exactly in that space, so worth a real-account pass before relying on tool use.

const index = Number(candidate);
const call = calls.get(index) ?? { name: '', arguments: '' };
if (typeof raw.id === 'string' && raw.id) call.id = raw.id;
if (typeof fn.name === 'string' && fn.name) call.name = fn.name;

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.

P2 — an inferred merge can rename a call and rebind another call's arguments. When a public-wire tool_calls entry lacks both id and index and calls.size <= 1, inferredIndex merges it into the sole existing call — then call.name = fn.name overwrites unconditionally and call.arguments += concatenates. A second distinct call {function: {name: 'B'}} with no arguments field executes tool B with tool A's arguments: it passes the toolNames check and JSON.parse succeeds on A's args. Reachability: provider-controlled stream. Fix: on an inferred merge, accept only fragments with no name or a matching name — a different non-empty name should take nextIndex or throw.

Comment thread packages/runtime/src/trae/model.ts Outdated
yield { type: 'raw', rawValue: { type: 'trae-heartbeat' } };
}
}
throw new Error('Trae stream disconnected before 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.

P2 — clean-EOF stream death classifies unknown → no retry. This throw (and 'Trae stream ended with an incomplete frame' in sse.ts) reaches isTruncatedStreamText, which requires BOTH 'stream disconnected before completion' AND the Codex-specific 'stream closed before response.completed' — Trae matches only the first → unknownMODEL_FAILURE_RETRY['unknown'] = null → dead turn. The OpenAI-family equivalents (stream_truncated, network) retry under the same condition: a Trae edge/proxy that cleanly closes the SSE stream without a done frame kills the turn where siblings would recover. Transport errors are already covered via undici cause codes → network, so the gap is only provider-clean-close/malformed-tail. Recommend mapping both throws to stream_truncated.

{flow.authRequestId && (
<Text type="supporting" color="secondary" role="status" aria-live="polite">
{!isXai && flow.stateHint
{props.service === 'codex' && flow.stateHint

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.

P2 — the employee-SSO device code is dropped by the UI. The employee path runs ByteCloud device authorization: cli_registration returns code, forwarded as stateHint/userCode (trae/authorization.ts:117) — but this branch only shows stateHint for service === 'codex', and use-connection-detail.ts passes showsDeviceCode: false for trae. The repo's own invariant one screen down says "Device pages without the code in their URL require the surface to show it"service_account_create_url doesn't embed the code, and the TUI shows stateHint for any provider (pi-tui-pickers.ts). If ByteCloud requires entering or verifying the code, employee SSO stalls until expiresAt. Fix is free and leaves the public flow untouched (stateHint === '' falls through): include trae in this branch and set showsDeviceCode: true. Reachability: normal user path — ByteDance SSO is a standard account option.

@Astro-Han

Copy link
Copy Markdown
Contributor

CI failed also a rebase is needed~

@chihumyum

Copy link
Copy Markdown
Contributor Author

Automated update from Codex.

@Astro-Han rebased onto current main (7baf8b053) and pushed 276330675. GitHub now reports the branch as mergeable with no conflicts.

  • Preserved the three P2 fixes and resolved the rebase integration issues: Trae Standard/Max Sessions retain their configured thinking defaults, and queued requests suppress the new process-header working cue. Both regressions were reproduced before the fixes and pass afterward. Protocol compatibility epoch is now 169.
  • The previous CI failure was the Side Chat lifecycle test's page.evaluate promise-collection error. It did not reproduce locally after the rebase: the focused case passed 3 consecutive runs, and the complete Desktop e2e suite passed 37/37 on macOS. No retries or timeouts were changed.
  • Build, lint, formatting, typecheck, Desktop/UI Knip, all affected workspace suites, architecture/epoch/inventory checks, and all 203 release contract tests pass locally.

The new test CI run plus audit and Windows recovery are running on this exact head. The PR description has the updated verification details. Please take another look when the checks finish.

…unts)

Loopback PKCE login for Trae public accounts (CN and SG, IDE and SOLO),
account-scoped model directories and inference through the native
llm_utils_chat route, plus the connection catalog and OAuth protocol
fields that carry the account variant.

Verified with real CN and SG accounts, IDE and SOLO; the findings are
folded in:

- The authorization page accepts only http://127.0.0.1:<port>/authorize
  as the callback, so the response is bound by login_trace_id instead of
  a random path.
- The token issuer is tried in order (growsg-normal, grow-normal) and
  recorded on the identity so refresh returns to it.
- Catalog page headers decode traeAccount; a header the protocol
  rejected took every connection in the catalog down with it.
- Tool schemas are projected onto the subset Gemini-backed routes
  accept; Trae forwards them as function_declarations and surfaced the
  rejections as stream error 4027.
- Stream errors keep the provider's message; 1005 (model outside the
  plan) and 4008 (quota) classify as billing rather than credentials.
- Model menu groups are headed by the account variant and numbered only
  when a variant repeats.
- US accounts are dropped and refused at login: the TTP gateway exposes
  only the IDE's create_agent_task protocol, not llm_utils_chat.
- Trae counts each machine/device pair against the account's device
  limit, and a login that invented a fresh pair every time spent a slot
  per sign-in until the issuer answered 20401 "Device limit reached".
  The pair is now derived per Host root, so every Trae Connection and
  re-login of one install is one device.
- The issuer loop reports the first issuer's verdict and stops on 401/403:
  the second issuer only ever saw a code the first had consumed, and its
  400 masked the real refusal. The verdict carries Trae's error code and
  a bounded message into the Host log and the sign-in toast.

Desktop refused an open_external presentation that carried no stateHint,
so a loopback PKCE provider never reached the browser and the attempt
ended as capability_unavailable. The wire contract leaves the hint
optional: an absent hint is not a mismatched request.

The Host now logs the redacted reason of a failed login attempt, and the
sign-in toast shows the failure code and its meaning instead of
flattening every code into "Authentication failed".

- OAuth login targets follow main's create/existing selection; the Trae
  account variant rides on a create target and is refused for every other
  provider, beside Codex's custom slug/name.
- The provider registry declares Trae through runtimeAdapter/modelDiscovery
  kinds; the runtime wire is `trae-raw-chat`.
- Provider queue events moved with the turn loop into ai-sdk-turn.
- The settings OAuth section, connection detail and desktop adapter use
  main's connection-settings bridge; the interim provider-accounts slice is
  gone.
- The model picker is main's Selector: Standard and Max routes list as two
  rows, and the on-open load-percentage reads were dropped with their IPC.
- Runtime Host compatibility epoch 167 (166 was taken by Connection usage
  reads on main).
- Trae account labels in the OAuth section come from the settings copy
  catalog rather than a locale prefix check.
Keep Trae account selection and its provider mark in connection-settings so
legacy settings dependencies and hooks stay within the architecture ratchet.
Preserve the shared login flow and cover account selection, pending login,
and Host-generation replacement with the real enrollment panel.

Vendor the MIT-licensed Lobe Icons Trae mark at a pinned revision, record its
digest and notice, and include the feature asset in source inventory checks.
Regenerate the renderer and Astryx inventories.

Generated-by: Codex
Reject tool fragments that would rename an existing call and reuse another
tool's arguments. Give clean EOF and incomplete frames a structured
truncated-stream error so the common model retry policy can recover.

Show employee SSO device codes in both enrollment and re-login while keeping
public accounts with an empty hint unchanged. Add regression coverage for
all three P2 findings and preserve valid anonymous tool fragments.

Include the Trae authorization sources in the Windows recovery path filter
so the release contract matches its executed test dependency closure.

Generated-by: Codex
Pass the Trae variant inventory into the new configured thinking-default
resolver, so Standard and Max Sessions inherit their declared defaults.
Cover both variants through the Session creation boundary.

Suppress the new process-header working cue while Trae queues a request,
keeping the queue position as the sole live activity indication.

Generated-by: Codex

@Astro-Han Astro-Han 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.

Re-reviewed at the rebased head — all findings from the previous round are resolved correctly:

  • Tool-call rebinding: the inferred merge now throws Trae tool call changed its name when a fragment's name disagrees with accumulated arguments — exactly the right boundary (repeats of the same name still merge, a different name can't steal arguments).
  • Clean-EOF truncation: TraeStreamTruncatedError at both EOF sites → code === 'TRAE_STREAM_TRUNCATED'stream_truncated in the classifier — now retried like the OpenAI-family equivalents.
  • Employee SSO device code: trae joins the stateHint display branch and showsDeviceCode is now true — public flow unaffected since stateHint === '' falls through.
  • Epoch: 170, correctly above main's current line, with the comment documenting the Trae fields.

The expanded account taxonomy (cn-solo/sg-solo) checks out too — region extraction (split('-')[0]) and the solo flag (endsWith('-solo')) compose correctly, SG issuer order (growsg → grow) is preserved, and each variant gets its own clientId/function set.

Remaining P3s from the first round stand as follow-ups (employee double-spelling canonicalization, absent-region-echo enforcement, bridge-contract traeAccount, refresh_failed copy mapping) — none blocking. The multi-turn tool-call caveat noted in the PR description still applies: worth a real-account pass when convenient.

@Astro-Han

Copy link
Copy Markdown
Contributor

Separate axis from the technical review (which stands) — a compliance question this needs to answer before merge.

What the integration actually does: it completes OAuth using Trae's own first-party client IDs — ono9krqynydwx5 (IDE) / en1oxy7wnw8j9n (SOLO), hardcoded in public-protocol.ts — rather than a registered third-party client, then calls llm_utils_chat, which is Trae's internal IDE inference endpoint, not a published API surface. I checked for a sanctioned channel: Trae's documented OAuth surfaces all run the other direction (enterprise SSO into Trae; Trae integrating third parties like Supabase), and there is no public developer program authorizing third-party clients to call these endpoints. The public ToS grants users a license for the official product — nothing authorizes a third-party client to reuse its client credentials.

Why it matters for this project: as an ASF project we can't ship an integration whose viability depends on the vendor's tolerance of credential reuse. The vendor can invalidate it at any point — client_id binding, attestation, or simply blocking llm_utils_chat for non-official callers — and the feature dies at runtime for users who already configured it. This is the same pattern vendors have enforced elsewhere (subscription OAuth tokens reused as third-party API access).

Two asks:

  1. Is there official authorization or a blessing from Trae/ByteDance for this integration — a registered client_id for Maka, or a documented OK? If yes, noting it here resolves the concern entirely.
  2. The ByteDance employee-SSO path is a sharper version of the same question: employee accounts exercised through a third-party client may also implicate the employer's own security policies for those users. Even if the public-account flow is tolerated, this piece probably needs its own answer.

If neither is authorized, my recommendation is not to merge this in its current form — either reduce it to whatever Trae publicly sanctions, or hold it until there's an official channel. Happy to be corrected if there's authorization context I'm missing.

@chihumyum chihumyum closed this Sep 20, 2026
@chihumyum

Copy link
Copy Markdown
Contributor Author

@Astro-Han
Thanks for the careful review and for clarifying that the authorization concern is separate from the technical assessment. I’d like to understand whether there is a path to reopening this PR.
For context, I’m a ByteDance employee. My motivation is to let employees who already have access to these services use an agent harness that better fits their workflows. I understand that my employment and account access do not, by themselves, establish authorization for this integration.
Would you be open to evaluating the public-account integration separately, with the employee SSO path removed from this PR pending clarification from the relevant service owners?
I’d also appreciate clarification on the project’s acceptance criteria. Maka’s current Codex provider reuses the official Codex client ID and directly accesses the Codex backend. Is there a documented authorization or compatibility basis for that integration that could serve as a reference for the evidence required here?
What specific confirmation from Trae/ByteDance would be sufficient—for example, a documented statement from the service owner, or a registered third-party client? Once we agree on the requirements, I’d be happy to narrow the scope and update the PR accordingly.

@chihumyum chihumyum reopened this Sep 21, 2026
@github-actions

Copy link
Copy Markdown

No description provided.

@chihumyum chihumyum closed this Sep 21, 2026
@chihumyum

Copy link
Copy Markdown
Contributor Author

@Astro-Han Nevermind. Closing this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants