Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ paste the redirect URL or authorization code back. During device approval that f
enter the displayed code on the provider's verification page instead. If the provider switches
to manual input, the dashboard replaces the old code and instructions on its next status poll.

If the proxy tries to open a browser and cannot, the login says so above the URL and keeps
going: open the sign-in page from the link or copy it. A device login never opens a browser by
itself; **Copy code & open** copies the code and opens the verification page in one click, and
the dashboard keeps waiting for as long as the provider's code stays valid. In the desktop app
these links open in your default browser.

To stop the proxy from opening a browser at all, tick **Don't open a browser on the proxy machine**
beside the login button, or set it permanently:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ runs helper features around provider requests.
| `emptyCompletionRetry?` | `boolean` | `false` | Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. `OCX_EMPTY_COMPLETION_RETRY=0` disables it without changing config; combo and routed-compaction turns remain excluded. |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Remove optional client-facing hints from canonical Codex Responses passthrough: the two `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` response headers, `response.metadata` events whose metadata type is `safety_buffering`, and top-level `safety_buffering` fields. Other headers, response data, policy refusals and failures are preserved. This does not disable provider safety enforcement or upstream buffering. Native `codex.response.metadata.headers` WebSocket metadata and `/responses/compact` are outside this filter. |
| `stallTimeoutSec?` | `number` | `300` (public) / disabled (local) | Seconds without meaningful upstream progress (Responses and native Chat) before the stream is cut. Unset, a **local** upstream (loopback, private, or a `.local`/`.lan` name) defaults to disabled and a public upstream to 300 s; a positive value applies to both (minimum 1 s); `0` disables the silence watchdog everywhere. Disabled leaves a silent-but-healthy local model connected (keep-alives still flow). Canonical ChatGPT Responses folded from SSE into non-streaming JSON retain a separate 15-minute whole-turn ceiling even when the silence watchdog is disabled. Pending `/v1/responses/compact` body reads share this budget but default to 300 s even for a local upstream — the route buffers the complete body while holding an active-turn lease — and an explicit value, including `0`, still wins. |
| `oauthOpenBrowser?` | `boolean` | `true` | Whether a login may open a browser on the machine running the proxy. Absent and `true` both open, so an existing install is unchanged; only an explicit `false` declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy's machine — the login still starts and the URL is still returned and displayed. `POST /api/oauth/login` and `POST /api/codex-auth/login` accept a per-request `openBrowser` boolean that overrides this, and the dashboard exposes the same choice beside the login button. Device-code flows never open a browser either way. |
| `oauthOpenBrowser?` | `boolean` | `true` | Whether a login may open a browser on the machine running the proxy. Absent and `true` both open, so an existing install is unchanged; only an explicit `false` declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy's machine — the login still starts and the URL is still returned and displayed. `POST /api/oauth/login` and `POST /api/codex-auth/login` accept a per-request `openBrowser` boolean that overrides this, and the dashboard exposes the same choice beside the login button. Both routes report the outcome as `browserLaunch`: `started`, `failed` (nothing could be opened; the dashboard says so), or `skipped`. Device-code flows never open a browser either way. |
| `connectTimeoutMs?` | `number` | `200000` | Per-attempt DNS/TCP/TLS/final-header deadline; it ends before body generation. |
| `shutdownTimeoutMs?` | `number` | `5000` | Graceful drain deadline before active turns are aborted. |
| `websockets?` | `boolean` | `false` | Advertise and admit the client-facing Responses WebSocket path. False keeps clients on HTTP/SSE; it does not disable an eligible canonical ChatGPT upstream WS optimization. Complete-input requests may reuse an upstream connection within the same selected credential, account, thread and turn; changed handshake policy or missing identity keeps requests on separate connections. This does not trim HTTP input or create previous-response IDs. |
Expand Down
1 change: 1 addition & 0 deletions gui/src/components/AddCodexAccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default function AddCodexAccountModal({
authUrl={ui.authUrl}
deviceCode={ui.deviceCode}
instructions={ui.instructions}
browserLaunch={ui.browserLaunch}
manualCode={ui.manualCode}
manualCodeBusy={manualCodeBusy}
manualCodeWaiting={manualCodeWaiting}
Expand Down
8 changes: 5 additions & 3 deletions gui/src/components/AddProviderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { baseUrlForChoice, matchChoiceId, resolvedBaseUrlForChoice } from "../ba
import { AddProviderOAuthPane } from "./add-provider-oauth-pane";
import { AddProviderFormPane } from "./add-provider-form-pane";
import { useAddProviderOAuth } from "./use-add-provider-oauth";
import type { BrowserLaunch } from "../oauth-browser-launch";
import {
addProviderModalReducer,
createInitialAddProviderState,
Expand Down Expand Up @@ -108,7 +109,7 @@ export default function AddProviderModal({
const usageRank = Object.fromEntries((usagePoll.data?.providers ?? []).map(row => [row.provider, row.requests]));
const {
preset, form, saving, error, oauthBusy, oauthMsg, oauthMsgTone, oauthUrl, oauthUrlProvider,
oauthDeviceCode, oauthInstructions,
oauthDeviceCode, oauthInstructions, oauthBrowserLaunch,
manualCode, manualCodeBusy, manualCodeMsg, manualCodeOk, endpointChoice, oauthTosPending,
} = state;

Expand Down Expand Up @@ -227,8 +228,8 @@ export default function AddProviderModal({
setOauthBusy: (busy: boolean) => dispatch({ type: "set-oauth-busy", busy }),
setOauthMsg: (msg: string) => dispatch({ type: "set-oauth-msg", msg }),
setOauthMsgTone: (tone: "ok" | "warn") => dispatch({ type: "set-oauth-tone", tone }),
setOauthUrl: (url: string, providerId: string, deviceCode?: string, instructions?: string) =>
dispatch({ type: "set-oauth-url", url, providerId, deviceCode, instructions }),
setOauthUrl: (url: string, providerId: string, deviceCode?: string, instructions?: string, browserLaunch?: BrowserLaunch) =>
dispatch({ type: "set-oauth-url", url, providerId, deviceCode, instructions, browserLaunch }),
setManualCode: (code: string) => dispatch({ type: "set-manual-code", code }),
setManualCodeMsg: (msg: string) => dispatch({ type: "set-manual-code-msg", msg }),
setManualCodeOk: (ok: boolean) => dispatch({ type: "set-manual-code-msg", msg: manualCodeMsg, ok }),
Expand Down Expand Up @@ -310,6 +311,7 @@ export default function AddProviderModal({
oauthUrl={oauthUrlProvider === preset.oauthProvider ? oauthUrl : ""}
oauthDeviceCode={oauthUrlProvider === preset.oauthProvider ? oauthDeviceCode : ""}
oauthInstructions={oauthUrlProvider === preset.oauthProvider ? oauthInstructions : ""}
oauthBrowserLaunch={oauthUrlProvider === preset.oauthProvider ? oauthBrowserLaunch : undefined}
manualCode={manualCode}
manualCodeBusy={manualCodeBusy}
manualCodeMsg={manualCodeMsg}
Expand Down
11 changes: 9 additions & 2 deletions gui/src/components/add-codex-account-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { BrowserLaunch } from "../oauth-browser-launch";

export type AddCodexAccountStep = "pick" | "oauth-waiting";
export type ManualCodeState = "idle" | "submitting" | "waiting";
export type StatusTone = "ok" | "warn";
Expand All @@ -11,6 +13,8 @@ export interface AddCodexAccountUiState {
deviceCode: string;
/** Provider-supplied prose that accompanies the code. */
instructions: string;
/** What the proxy reported about opening the browser for this login. */
browserLaunch?: BrowserLaunch;
manualCode: string;
manualCodeState: ManualCodeState;
statusNotice: string;
Expand All @@ -37,7 +41,7 @@ export type AddCodexAccountUiAction =
| { type: "set-id"; id: string }
| { type: "set-error"; error: string }
| { type: "set-auth-url"; authUrl: string }
| { type: "set-login-hint"; authUrl: string; deviceCode?: string; instructions?: string }
| { type: "set-login-hint"; authUrl: string; deviceCode?: string; instructions?: string; browserLaunch?: BrowserLaunch }
| { type: "set-manual-code"; manualCode: string }
| { type: "set-manual-code-state"; manualCodeState: ManualCodeState }
| { type: "set-status-notice"; statusNotice: string; statusTone?: StatusTone }
Expand All @@ -62,6 +66,7 @@ export function addCodexAccountUiReducer(state: AddCodexAccountUiState, action:
authUrl: action.authUrl,
deviceCode: action.deviceCode ?? "",
instructions: action.instructions ?? "",
browserLaunch: action.browserLaunch,
};
case "set-manual-code":
return { ...state, manualCode: action.manualCode };
Expand All @@ -74,7 +79,9 @@ export function addCodexAccountUiReducer(state: AddCodexAccountUiState, action:
case "clear-manual-code":
return { ...state, manualCode: "", manualCodeState: "idle", statusNotice: "", statusTone: "ok" };
case "reset-oauth-start":
return { ...state, error: "", statusNotice: "", statusTone: "ok", flowId: null };
// The launch outcome belongs to the login being replaced: a restart (for instance into the
// device flow after a failed launch) must not keep warning about a browser it never tried.
return { ...state, error: "", statusNotice: "", statusTone: "ok", flowId: null, browserLaunch: undefined };
case "oauth-code-submitted":
return { ...state, error: "", manualCode: "", manualCodeState: "waiting", statusTone: "ok", statusNotice: "" };
default:
Expand Down
5 changes: 4 additions & 1 deletion gui/src/components/add-codex-account-waiting-step.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useT } from "../i18n/shared";
import { LoginHint } from "./login-url-block";
import type { BrowserLaunch } from "../oauth-browser-launch";
import type { StatusTone } from "./add-codex-account-reducer";

export function AddCodexAccountWaitingStep({
reauthAccountId,
authUrl,
deviceCode,
instructions,
browserLaunch,
manualCode,
manualCodeBusy,
manualCodeWaiting,
Expand All @@ -24,6 +26,7 @@ export function AddCodexAccountWaitingStep({
/** Short human code when the login is a device flow; empty otherwise. */
deviceCode?: string;
instructions?: string;
browserLaunch?: BrowserLaunch;
manualCode: string;
manualCodeBusy: boolean;
manualCodeWaiting: boolean;
Expand All @@ -44,7 +47,7 @@ export function AddCodexAccountWaitingStep({
<h3 style={{ marginBottom: 4 }}>{reauthAccountId ? t("codexAuth.reauthenticate") : t("codexAuth.oauthLogin")}</h3>
<p className="modal-desc">{t("codexAuth.oauthWaiting")}</p>
<LoginHint
hint={{ url: authUrl, deviceCode, instructions }}
hint={{ url: authUrl, deviceCode, instructions, browserLaunch }}
paste={{
value: manualCode,
busy: manualCodeBusy,
Expand Down
15 changes: 13 additions & 2 deletions gui/src/components/add-provider-modal-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CatalogPreset } from "./provider-catalog/provider-presets";
import type { ProviderPayloadForm } from "../provider-payload";
import type { BrowserLaunch } from "../oauth-browser-launch";

type Preset = CatalogPreset;
type FormState = ProviderPayloadForm;
Expand All @@ -18,6 +19,8 @@ export type AddProviderModalState = {
oauthDeviceCode: string;
/** Provider-supplied prose for the in-flight login. */
oauthInstructions: string;
/** What the proxy reported about opening the browser for the in-flight login. */
oauthBrowserLaunch?: BrowserLaunch;
/** Provider the pending `oauthUrl` belongs to; a late response for a switched-away provider must not render. */
oauthUrlProvider: string | null;
manualCode: string;
Expand All @@ -38,7 +41,7 @@ export type AddProviderModalAction =
| { type: "set-oauth-busy"; busy: boolean }
| { type: "set-oauth-msg"; msg: string; tone?: "ok" | "warn" }
| { type: "set-oauth-tone"; tone: "ok" | "warn" }
| { type: "set-oauth-url"; url: string; providerId: string; deviceCode?: string; instructions?: string }
| { type: "set-oauth-url"; url: string; providerId: string; deviceCode?: string; instructions?: string; browserLaunch?: BrowserLaunch }
| { type: "set-manual-code"; code: string }
| { type: "set-manual-code-busy"; busy: boolean }
| { type: "set-manual-code-msg"; msg: string; ok?: boolean }
Expand Down Expand Up @@ -97,6 +100,7 @@ export function addProviderModalReducer(
oauthUrl: "",
oauthDeviceCode: "",
oauthInstructions: "",
oauthBrowserLaunch: undefined,
oauthUrlProvider: null,
oauthBusy: false,
manualCode: "",
Expand All @@ -115,6 +119,7 @@ export function addProviderModalReducer(
oauthUrl: "",
oauthDeviceCode: "",
oauthInstructions: "",
oauthBrowserLaunch: undefined,
oauthUrlProvider: null,
oauthBusy: false,
manualCode: "",
Expand Down Expand Up @@ -145,6 +150,11 @@ export function addProviderModalReducer(
oauthDeviceCode: action.deviceCode ?? "",
oauthInstructions: action.instructions ?? "",
oauthUrlProvider: action.providerId,
// The launch outcome arrives once, with the POST; a later status hint for the same login
// omits it and must not erase it. Clearing the URL ends the login and its outcome.
oauthBrowserLaunch: !action.url
? undefined
: action.browserLaunch ?? (state.oauthUrlProvider === action.providerId ? state.oauthBrowserLaunch : undefined),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
case "set-manual-code":
return { ...state, manualCode: action.code };
Expand All @@ -155,7 +165,7 @@ export function addProviderModalReducer(
case "set-oauth-tos-pending":
return { ...state, oauthTosPending: action.providerId };
case "use-oauth-login":
return { ...state, form: action.form, error: "", oauthUrl: "", oauthDeviceCode: "", oauthInstructions: "", oauthUrlProvider: null };
return { ...state, form: action.form, error: "", oauthUrl: "", oauthDeviceCode: "", oauthInstructions: "", oauthBrowserLaunch: undefined, oauthUrlProvider: null };
case "use-api-key-instead":
return {
...state,
Expand All @@ -165,6 +175,7 @@ export function addProviderModalReducer(
oauthUrl: "",
oauthDeviceCode: "",
oauthInstructions: "",
oauthBrowserLaunch: undefined,
oauthUrlProvider: null,
oauthBusy: false,
manualCode: "",
Expand Down
5 changes: 4 additions & 1 deletion gui/src/components/add-provider-oauth-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IconLock } from "../icons";
import { useT } from "../i18n/shared";
import { LoginHint } from "./login-url-block";
import type { BrowserLaunch } from "../oauth-browser-launch";
import { OpenBrowserPrefToggle } from "./open-browser-pref-toggle";
import type { CatalogPreset } from "./provider-catalog/provider-presets";

Expand All @@ -13,6 +14,7 @@ export function AddProviderOAuthPane({
oauthUrl,
oauthDeviceCode,
oauthInstructions,
oauthBrowserLaunch,
manualCode,
manualCodeBusy,
manualCodeMsg,
Expand All @@ -32,6 +34,7 @@ export function AddProviderOAuthPane({
oauthUrl: string;
oauthDeviceCode?: string;
oauthInstructions?: string;
oauthBrowserLaunch?: BrowserLaunch;
manualCode: string;
manualCodeBusy: boolean;
manualCodeMsg: string;
Expand Down Expand Up @@ -68,7 +71,7 @@ export function AddProviderOAuthPane({
)}
{oauthBusy && (
<LoginHint
hint={{ url: oauthUrl, deviceCode: oauthDeviceCode, instructions: oauthInstructions }}
hint={{ url: oauthUrl, deviceCode: oauthDeviceCode, instructions: oauthInstructions, browserLaunch: oauthBrowserLaunch }}
paste={{
value: manualCode,
busy: manualCodeBusy,
Expand Down
11 changes: 5 additions & 6 deletions gui/src/components/codex-account-pool-main-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CodexAccountEntry, CodexAccountLoadState } from "./codex-account-p
import type { CodexAccountModeState } from "../codex-multi-state";
import type { TFn } from "../i18n/shared";
import type { MainDeviceReauthState } from "./use-main-device-reauth";
import { LoginHint } from "./login-url-block";
import type { NoticeTone } from "../ui";
import { navigateHash } from "../hash-routing";
import {
Expand Down Expand Up @@ -228,12 +229,10 @@ export function CodexAccountPoolMainCard({
)}
{mainReauth && (mainReauth.state.phase === "pending" || mainReauth.state.phase === "committing") && (
<span className="codex-main-reauth-pending">
{mainReauth.state.verificationUrl && (
<span>{t("codexAuth.mainReauthOpen")}: {mainReauth.state.verificationUrl}</span>
)}
{mainReauth.state.deviceCode && (
<strong>{t("codexAuth.mainReauthCode")}: {mainReauth.state.deviceCode}</strong>
)}
{/* The shared renderer every other login surface uses: a copyable code, a
selectable and copyable URL, and one click to copy the code and open the
page. Plain text here left the user retyping both by hand. */}
<LoginHint hint={{ url: mainReauth.state.verificationUrl, deviceCode: mainReauth.state.deviceCode }} />
<span className="faint">{t("codexAuth.mainReauthPending")}</span>
{mainReauth.state.cancelFailed && (
<span role="status" className="badge badge-amber">{t("codexAuth.mainReauthFailed")}</span>
Expand Down
Loading
Loading