Skip to content

refactor: modularize 12.8K-line providers/[id]/page.tsx (Issue #3594) - #3627

Closed
oyi77 wants to merge 24 commits into
cheaper-inference:release/v3.8.23from
oyi77:refactor/modularize-providers-page
Closed

oyi77 wants to merge 24 commits into
cheaper-inference:release/v3.8.23from
oyi77:refactor/modularize-providers-page

Conversation

@oyi77

@oyi77 oyi77 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR addresses the highest priority massive file from Issue #3594: src/app/(dashboard)/dashboard/providers/[id]/page.tsx at 12,882 lines — the largest file in the entire codebase.

Changes

Splits the monolithic page component into 6 domain-focused files organized by responsibility:

File Lines Contents
types.ts 392 All shared interfaces and type definitions
utils.tsx 1,377 Helper functions + utility components (ModelSourceBadge, CooldownTimer, ModelCompatPopover, etc.)
modelManagement.tsx 1,645 ModelRow, ModelVisibilityToolbar, PassthroughModelsSection, CustomModelsSection, CompatibleModelsSection
connectionManagement.tsx 2,038 ConnectionRow, EditConnectionModal, EditCompatibleNodeModal
authModals.tsx 2,909 AddApiKeyModal, SiliconFlowEndpointModal, Import/Apply auth modals (Codex/Claude/Gemini)
playground.tsx 118 ProviderPlaygroundPanel, renderKindPanel

Results

  • page.tsx: 12,882 → 4,687 lines (63% reduction)
  • Each extracted file is < 3,000 lines and focused on a single domain
  • Imports are automatically pruned per file — no unused imports
  • Full backward compatibility: every symbol is re-exported via import

Verification

  • npm run typecheck:core — passes
  • ESLint — 0 errors, only 3 pre-existing hook-dependency warnings in page.tsx
  • No behavioral changes — strict structural refactoring only

Resolves part of #3594.

diegosouzapw and others added 22 commits June 10, 2026 17:20
…ce#3598)

Integrated into release/v3.8.21 — chatCore phase modularization. Adjusted: re-derive idempotencyKey for the save path after the check moved into the module (co-authored). Thanks @oyi77!
…on) + cheaper-inference#3588 (combo reasoning buffer)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…drop shadow/guardrails doc-fiction (cheaper-inference#3496) (cheaper-inference#3602)

Integrated into release/v3.8.21 — implements GET /api/guardrails + POST /api/guardrails/test, removes shadow/guardrails doc-fiction. TDD-validated (5/5) + check-docs-symbols/typecheck/eslint green.
Split-out PR C from cheaper-inference#3584. Isolates textual reasoning wrappers (<think>/<thinking>/<thought>/<internal_thought>, including malformed/open tags) into reasoning_content across both the non-streaming sanitizer and the Gemini streaming translator, with split-chunk buffering. Additive to the existing textual tool-call pipeline; does not touch the cheaper-inference#3569 native functionResponse path. Integrated into release/v3.8.21. Thanks @dhaern!
…nce#3603)

Split-out PR A from cheaper-inference#3584. Normalizes the Antigravity/agy Gemini 3.5 Flash tier IDs to clean public names (gemini-3.5-flash-low/medium/high), maps them to the live upstream IDs at the executor boundary, and removes Antigravity from the global model resolver so the executor owns wire normalization. Maintainer follow-up: kept gemini-3.5-flash-preview as a hidden backward-compat alias routing to the High tier (so saved combos/configs keep working). Live-validated the tier set via the agy CLI catalog. Integrated into release/v3.8.21. Thanks @dhaern!
…ference#3604)

Split-out PR B from cheaper-inference#3584. Normalizes Antigravity/agy provider quotas: prefers retrieveUserQuota for live consumption, falls back to fetchAvailableModels and local usage_history, sanitizes cached Provider Limits so retired upstream IDs are not re-exposed, and schedules a deduplicated post-usage refresh. Maintainer follow-up: decoupled the post-usage refresh via a lightweight usageEvents bus (usageHistory no longer dynamic-imports providerLimits) so it does not pull the executors/translator graph into the typecheck-core surface — typecheck:core stays at 0. Integrated into release/v3.8.21. Thanks @dhaern!
…aper-inference#3604 (provider quotas) + cheaper-inference#3605 (reasoning wrappers)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…nference#3594)

Breaks the 12,882-line providers/[id]/page.tsx into 6 domain-focused files:

- types.ts — all shared interfaces/types
- utils.tsx — helper functions and utility components (ModelSourceBadge, CooldownTimer, ModelCompatPopover, etc.)
- modelManagement.tsx — ModelRow, ModelVisibilityToolbar, PassthroughModelsSection, CustomModelsSection, CompatibleModelsSection
- connectionManagement.tsx — ConnectionRow, EditConnectionModal, EditCompatibleNodeModal
- authModals.tsx — AddApiKeyModal, SiliconFlowEndpointModal, Import/Apply (Codex/Claude/Gemini) auth modals
- playground.tsx — ProviderPlaygroundPanel, renderKindPanel

page.tsx reduced from 12,882 → 4,687 lines (63% reduction).
TypeScript typecheck passes cleanly. ESLint: 0 errors, only 3 pre-existing warnings.
Pre-parse and de-duplicate all imports per file to eliminate unused imports.

Resolves part of cheaper-inference#3594.

@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 soft-deprecates @omniroute/opencode-provider in favor of @omniroute/opencode-plugin, introduces CLI shorthands for autostart, implements real guardrail API endpoints, secures public OAuth client IDs, adds a max_tokens buffer for reasoning models, and translates chat responses back to the legacy text-completion shape for /v1/completions. The code review feedback highlights critical issues, including potential browser-side ReferenceErrors due to Buffer usage in client-side components, strict-mode TypeScript compilation errors from unknown type annotations in the modularized chatCore handlers, unnecessary CPU overhead from infinite interval timers, and a state-mutation bug in the single-model execution path.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1231 to +1240
function extractEmailFromJwtLocal(idToken: string): string | null {
try {
const parts = idToken.split(".");
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
return typeof payload.email === "string" ? payload.email : null;
} catch {
return null;
}
}

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.

critical

Using Buffer in a client-side React component ("use client") will cause a runtime ReferenceError: Buffer is not defined in browser environments where Buffer is not globally polyfilled. Since this is a client-side utility file, we should use the standard browser-safe atob function to decode the base64url-encoded JWT payload.

function extractEmailFromJwtLocal(idToken: string): string | null {
  try {
    const parts = idToken.split(".");
    if (parts.length !== 3) return null;
    const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
    const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
    const payload = JSON.parse(atob(padded));
    return typeof payload.email === "string" ? payload.email : null;
  } catch {
    return null;
  }
}

Comment on lines +1323 to +1332
function extractEmailFromGeminiJwt(idToken: string): string | null {
try {
const parts = idToken.split(".");
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
return typeof payload.email === "string" ? payload.email : null;
} catch {
return null;
}
}

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.

critical

Using Buffer in a client-side React component ("use client") will cause a runtime ReferenceError: Buffer is not defined in browser environments where Buffer is not globally polyfilled. Since this is a client-side utility file, we should use the standard browser-safe atob function to decode the base64url-encoded JWT payload.

function extractEmailFromGeminiJwt(idToken: string): string | null {
  try {
    const parts = idToken.split(".");
    if (parts.length !== 3) return null;
    const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
    const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
    const payload = JSON.parse(atob(padded));
    return typeof payload.email === "string" ? payload.email : null;
  } catch {
    return null;
  }
}

Comment on lines +13 to +19
clientRawRequest: unknown;
provider: string;
model: string;
effectiveServiceTier: unknown;
startTime: number;
log: unknown;
}) {

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

Typing clientRawRequest and log as unknown will cause TypeScript compilation errors under strict mode when accessing properties like clientRawRequest?.headers or calling log?.debug?.(...). We should type them as any to allow dynamic property access and method calls.

Suggested change
clientRawRequest: unknown;
provider: string;
model: string;
effectiveServiceTier: unknown;
startTime: number;
log: unknown;
}) {
clientRawRequest: any;
provider: string;
model: string;
effectiveServiceTier: any;
startTime: number;
log: any;
}) {

Comment on lines +27 to +39
semanticCacheEnabled: boolean;
body: Record<string, unknown>;
clientRawRequest: unknown;
model: string;
provider: string;
stream: boolean;
reqLogger: unknown;
effectiveServiceTier: unknown;
connectionId: string | null;
startTime: number;
log: unknown;
persistAttemptLogs: (args: unknown) => void;
}) {

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

Typing body as Record<string, unknown>, and clientRawRequest, reqLogger, and log as unknown will cause TypeScript compilation errors under strict mode when accessing properties like body.messages, clientRawRequest?.headers, or calling reqLogger.logConvertedResponse(...) and log?.debug?.(...). We should type them as any to allow dynamic property access and method calls.

Suggested change
semanticCacheEnabled: boolean;
body: Record<string, unknown>;
clientRawRequest: unknown;
model: string;
provider: string;
stream: boolean;
reqLogger: unknown;
effectiveServiceTier: unknown;
connectionId: string | null;
startTime: number;
log: unknown;
persistAttemptLogs: (args: unknown) => void;
}) {
semanticCacheEnabled: boolean;
body: any;
clientRawRequest: any;
model: string;
provider: string;
stream: boolean;
reqLogger: any;
effectiveServiceTier: any;
connectionId: string | null;
startTime: number;
log: any;
persistAttemptLogs: (args: any) => void;
}) {

Comment on lines +27 to +36
}: {
body: Record<string, unknown>;
memoryOwnerId: string | null;
provider: string;
effectiveModel: string;
sourceFormat: string;
targetFormat: string;
backgroundReason: string | null;
log: unknown;
}) {

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

Typing body as Record<string, unknown> and log as unknown will cause TypeScript compilation errors under strict mode when accessing properties like body.messages, body.input, body.tools, or calling log?.debug?.(...). We should type them as any to allow dynamic property access and method calls.

Suggested change
}: {
body: Record<string, unknown>;
memoryOwnerId: string | null;
provider: string;
effectiveModel: string;
sourceFormat: string;
targetFormat: string;
backgroundReason: string | null;
log: unknown;
}) {
}: {
body: any;
memoryOwnerId: string | null;
provider: string;
effectiveModel: string;
sourceFormat: string;
targetFormat: string;
backgroundReason: string | null;
log: any;
}) {

Comment on lines +4 to +8
export function sanitizeChatRequestBody(
body: Record<string, unknown>,
sourceFormat: string,
targetFormat: string
): Record<string, unknown> {

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

Typing body as Record<string, unknown> will cause TypeScript compilation errors under strict mode when accessing, mutating, or deleting properties like body.max_output_tokens, body.max_completion_tokens, body.max_tokens, body.messages, body.input, and body.tools. We should type body as any to allow dynamic property access and mutations.

Suggested change
export function sanitizeChatRequestBody(
body: Record<string, unknown>,
sourceFormat: string,
targetFormat: string
): Record<string, unknown> {
export function sanitizeChatRequestBody(
body: any,
sourceFormat: string,
targetFormat: string
): any {

Comment on lines +197 to +210
useEffect(() => {
const checkCooldown = () => {
const cooldown =
connection.rateLimitedUntil && new Date(connection.rateLimitedUntil).getTime() > Date.now();
setIsCooldown(cooldown);
};

checkCooldown();
// Update every second while in cooldown
const interval = connection.rateLimitedUntil ? setInterval(checkCooldown, 1000) : null;
return () => {
if (interval) clearInterval(interval);
};
}, [connection.rateLimitedUntil]);

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 connection.rateLimitedUntil is in the past, the interval timer is still created and runs every second indefinitely, repeatedly setting isCooldown to false. This causes unnecessary CPU overhead. We should only create the interval if the cooldown is actually in the future, and clear it as soon as the cooldown expires.

  useEffect(() => {
    let interval: any = null;
    const checkCooldown = () => {
      const cooldown =
        connection.rateLimitedUntil && new Date(connection.rateLimitedUntil).getTime() > Date.now();
      setIsCooldown(cooldown);
      if (!cooldown && interval) {
        clearInterval(interval);
        interval = null;
      }
    };

    checkCooldown();
    const cooldownActive =
      connection.rateLimitedUntil && new Date(connection.rateLimitedUntil).getTime() > Date.now();
    if (cooldownActive) {
      interval = setInterval(checkCooldown, 1000);
    }
    return () => {
      if (interval) clearInterval(interval);
    };
  }, [connection.rateLimitedUntil]);

Comment on lines +1350 to +1372
useEffect(() => {
const updateRemaining = () => {
const diff = new Date(until).getTime() - Date.now();
if (diff <= 0) {
setRemaining("");
return;
}
const secs = Math.floor(diff / 1000);
if (secs < 60) {
setRemaining(`${secs}s`);
} else if (secs < 3600) {
setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`);
} else {
const hrs = Math.floor(secs / 3600);
const mins = Math.floor((secs % 3600) / 60);
setRemaining(`${hrs}h ${mins}m`);
}
};

updateRemaining();
const interval = setInterval(updateRemaining, 1000);
return () => clearInterval(interval);
}, [until]);

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 cooldown has already expired initially (or once it expires), the setInterval timer continues to run every second indefinitely. We should only create the interval if the cooldown is in the future, and clear it as soon as the remaining time reaches zero.

  useEffect(() => {
    let interval: any = null;
    const updateRemaining = () => {
      const diff = new Date(until).getTime() - Date.now();
      if (diff <= 0) {
        setRemaining("");
        if (interval) {
          clearInterval(interval);
          interval = null;
        }
        return;
      }
      const secs = Math.floor(diff / 1000);
      if (secs < 60) {
        setRemaining(`${secs}s`);
      } else if (secs < 3600) {
        setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`);
      } else {
        const hrs = Math.floor(secs / 3600);
        const mins = Math.floor((secs % 3600) / 60);
        setRemaining(`${hrs}h ${mins}m`);
      }
    };

    updateRemaining();
    const isActive = new Date(until).getTime() > Date.now();
    if (isActive) {
      interval = setInterval(updateRemaining, 1000);
    }
    return () => {
      if (interval) clearInterval(interval);
    };
  }, [until]);

Comment on lines +3581 to +3596
if (supportsReasoning(modelStr)) {
const bodyRecord = attemptBody as Record<string, unknown>;
const currentMaxTokens = Number(bodyRecord.max_tokens) || 0;
if (currentMaxTokens > 0) {
// Add 50% buffer + 1000 floor to ensure reasoning + content both fit
const bufferedMaxTokens = Math.max(
currentMaxTokens + 1000,
Math.ceil(currentMaxTokens * 1.5)
);
bodyRecord.max_tokens = bufferedMaxTokens;
log.info(
"COMBO",
`Reasoning model ${modelStr}: buffered max_tokens ${currentMaxTokens} -> ${bufferedMaxTokens}`
);
}
}

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 the single-model section, attemptBody is cast to Record<string, unknown> and mutated directly (bodyRecord.max_tokens = bufferedMaxTokens;). This can cause the mutation to persist and potentially compound across retries or sibling calls. To prevent any side-effects, we should apply the buffer to a shallow copy of attemptBody, just like in the round-robin section.

          if (supportsReasoning(modelStr)) {
            const currentMaxTokens = Number((attemptBody as Record<string, unknown>).max_tokens) || 0;
            if (currentMaxTokens > 0) {
              const bufferedMaxTokens = Math.max(
                currentMaxTokens + 1000,
                Math.ceil(currentMaxTokens * 1.5)
              );
              attemptBody = {
                ...(attemptBody as Record<string, unknown>),
                max_tokens: bufferedMaxTokens,
              } as typeof attemptBody;
              log.info(
                "COMBO",
                `Reasoning model ${modelStr}: buffered max_tokens ${currentMaxTokens} -> ${bufferedMaxTokens}`
              );
            }
          }

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi @oyi77 — thank you for taking on the single biggest file in the repo. Tackling providers/[id]/page.tsx (12,882 LOC) is exactly the kind of high-impact cleanup the project needs, and we're going to leverage part of this effort as we land the decomposition.

To keep everyone aligned, here's a detailed picture of what this PR does versus what's planned and now in progress under the tracking issue #3501 (which carries the owner-approved 6-phase plan).

What this PR does

  • Splits page.tsx into 6 sibling files (types.ts, utils.tsx, modelManagement.tsx, connectionManagement.tsx, authModals.tsx, playground.tsx).
  • Reduces page.tsx from 12,882 → 4,687 LOC, with symbols re-exported for backward compatibility.
  • typecheck:core passes; ESLint clean apart from pre-existing warnings.

What the #3501 plan calls for (and how we're landing it)

The registered plan follows the repo's existing big-page convention and a strangler-fig sequence so each step is independently shippable and the check-file-size ratchet drops monotonically:

  1. Thin page.tsx → ProviderDetailPageClient.tsx wrapper (the convention used by EndpointPageClient/ApiManagerPageClient), rather than keeping a 4,687-LOC page.tsx.
  2. Colocated components/ + hooks/ subfolders (the providers/ route already uses this), rather than flat sibling files.
  3. Extracting the data-fetching/state into custom hooks (useProviderConnections, useProviderModelMeta, …) so the ~195 useState / 87 inline fetch() calls collapse behind typed hooks — this is the part that actually de-risks future edits.
  4. A smoke render test first (the page had zero tests) as the safety net every extraction phase is diffed against — Hard Rule fix(oauth): prevent connection test from corrupting valid tokens #8.
  5. One phase per PR, each lowering the frozen check-file-size entry, so the file shrinks monotonically toward the ≤800-LOC cap.

We've started shipping these against the active release/v3.8.22 branch: Phase 0 (thin ProviderDetailPageClient + first smoke test) and Phase 1a (extracting the Codex/Claude/Gemini auth-import modal clusters into components/modals/) are merged, with later phases (presentational sub-components, helpers→lib/, hooks, render split) following the same pattern. You're credited as co-author on these for the parallel effort.

One heads-up on this branch

This PR currently targets main and its base predates the v3.8.21/v3.8.22 release work, so its diff also reverts ~70 unrelated files (recent release hardening, i18n changelogs, etc.). That, plus the structural differences above, is why we're aligning to the phased plan rather than merging this branch directly.

Thanks again — the grouping of symbols here was a useful reference. Follow #3501 to track the remaining phases.


This is an automated message from the quality-gate workflow.

oyi77 added 2 commits June 11, 2026 19:09
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
… hydration, secure upstream-ca test endpoint
This was referenced Jun 11, 2026
@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.23 June 12, 2026 01:19
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi @oyi77 — thank you for taking the time to tackle this big refactor 🙏

We've now completed the providers/[id] god-component decomposition directly on the release branch, shipped in v3.8.22 under issue #3501, using a phased strangler-fig approach:

  • Phase 0–1b: thin page.tsx → ProviderDetailPageClient + auth-import modals + EditCompatibleNodeModal
  • Phase 2: pure helpers → cycle-safe leaf module
  • Phase 1c: AddApiKeyModal + EditConnectionModal
  • Phase 1d: ConnectionRow + ModelCompatPopover + SiliconFlowEndpointModal
  • Phase 1e: useModelCompatState hook + the model sections
  • Phase 1f: useProviderConnections / useProviderSettings / useProviderModels hooks

Result: 12,882 → 4,062 LOC (−68%) so far, fully cycle-checked, with smoke tests at every step and the file-size ratchet enforcing it stays shrunk.

Because the work landed phase-by-phase on the release branch (each phase independently typechecked/tested/merged), this PR is now superseded — so I'm closing it. You're credited as co-author on every #3501 phase commit, so your contribution is on the record. Thanks again for pushing on this, and please keep the PRs coming! 🚀

oyi77 added a commit to oyi77/OmniRoute that referenced this pull request Jun 12, 2026
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
oyi77 added a commit to oyi77/OmniRoute that referenced this pull request Jun 12, 2026
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
oyi77 added a commit to oyi77/OmniRoute that referenced this pull request Jun 12, 2026
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
oyi77 added a commit to oyi77/OmniRoute that referenced this pull request Jun 12, 2026
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
oyi77 added a commit to oyi77/OmniRoute that referenced this pull request Jun 12, 2026
- Replaced Buffer with browser-safe atob in utils.tsx
- Fixed memory leaks/infinite intervals in cooldown timers
- Typed chatCore handlers with 'any' instead of 'unknown' to fix strict-mode property access
- Fixed single-model reasoning payload mutation in combo.ts
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.

4 participants