Skip to content

Commit 46a8326

Browse files
feat: add ChatGPT Pro OAuth as LLM provider (#476)
* feat: add ChatGPT Pro OAuth as LLM provider Adds OAuth 2.0 (Authorization Code + PKCE) flow so users can authenticate with their ChatGPT Pro subscription to power BrowserOS's agent, matching the pattern used by Codex CLI, OpenCode, and Pi. Server: - OAuth token lifecycle (PKCE, exchange, refresh, SQLite storage) - Dedicated callback server on port 1455 (Codex client ID registration) - Codex fetch wrapper routing API calls to chatgpt.com/backend-api - Config resolution + provider factories for all code paths (chat, test, refine) Extension: - ChatGPT Pro template card with OAuth flow trigger - Status polling hook + auto-create provider on auth success - Model list with Codex-supported models (gpt-5.x-codex family) * fix: address Greptile PR review comments - Wire OAuth callback server stop handle into onShutdown (P1: port 1455 leak) - Guard against missing refresh token + clear stale tokens on failed refresh (P1) - Add logger.warn to silent catch in codex-fetch body mutation - Document JWT trust assumption in parseAccessTokenClaims - Source model ID from provider template instead of hard-coding * simplify: remove unnecessary OAuth shutdown wiring and useCallback - Revert OAuthHandle interface — callback server port releases on process exit - Remove stopCallbackServer from shutdown flow (dead code) - Remove all useCallback from useOAuthStatus per CLAUDE.md guidance * style: add readonly modifiers and braces per TS style guide * docs: add E2E test screenshots for ChatGPT Pro OAuth * fix: strip item IDs from Codex requests to fix multi-turn conversations * fix: preserve function_call_output IDs in Codex requests * fix: resolve Codex store=false + tool-use incompatibility - Pass providerOptions { openai: { store: false } } to ToolLoopAgent so the AI SDK inlines content instead of using item_reference - Strip item IDs and previous_response_id in codex-fetch (safety net) - Use .responses() model (Codex only speaks Responses API format) * fix: remove non-Codex model gpt-5.2 from chatgpt-pro model list * fix: strip unsupported Codex params and update model list - Strip temperature, max_tokens, top_p from Codex requests (unsupported) - Add all available Codex models including gpt-5.4, gpt-5.2, gpt-5.1 * chore: remove screenshots containing email * feat: enable reasoning events for ChatGPT Pro Codex models * chore: set reasoning effort to high for ChatGPT Pro * feat: add configurable reasoning effort and summary for ChatGPT Pro - Add reasoningEffort (none/low/medium/high) and reasoningSummary (auto/concise/detailed) dropdowns in the Edit Provider dialog - Pass through extension → chat request → agent config → providerOptions - Defaults: effort=high, summary=auto * fix: strip max_output_tokens from Codex requests (fixes compaction) * fix: address Greptile P1 issues - Fix default model fallback: gpt-4o → gpt-5.3-codex (Codex endpoint) - Clear stale tokens on refresh failure (prevents infinite retry loop) - Only auto-create provider after explicit OAuth flow, not on page load - Add catch block to auto-create effect with error toast
1 parent 4b18723 commit 46a8326

32 files changed

Lines changed: 1158 additions & 23 deletions

packages/browseros-agent/apps/agent/entrypoints/app/ai-settings/AISettingsPage.tsx

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useQueryClient } from '@tanstack/react-query'
2-
import { type FC, useMemo, useState } from 'react'
2+
import { type FC, useEffect, useMemo, useRef, useState } from 'react'
33
import { toast } from 'sonner'
44
import {
55
AlertDialog,
@@ -13,14 +13,24 @@ import {
1313
} from '@/components/ui/alert-dialog'
1414
import { useSessionInfo } from '@/lib/auth/sessionStorage'
1515
import { useAgentServerUrl } from '@/lib/browseros/useBrowserOSProviders'
16+
import {
17+
CHATGPT_PRO_OAUTH_COMPLETED_EVENT,
18+
CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT,
19+
CHATGPT_PRO_OAUTH_STARTED_EVENT,
20+
} from '@/lib/constants/analyticsEvents'
1621
import { GetProfileIdByUserIdDocument } from '@/lib/conversations/graphql/uploadConversationDocument'
1722
import { getQueryKeyFromDocument } from '@/lib/graphql/getQueryKeyFromDocument'
1823
import { useGraphqlMutation } from '@/lib/graphql/useGraphqlMutation'
1924
import { useGraphqlQuery } from '@/lib/graphql/useGraphqlQuery'
20-
import type { ProviderTemplate } from '@/lib/llm-providers/providerTemplates'
25+
import {
26+
getProviderTemplate,
27+
type ProviderTemplate,
28+
} from '@/lib/llm-providers/providerTemplates'
2129
import { testProvider } from '@/lib/llm-providers/testProvider'
2230
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
2331
import { useLlmProviders } from '@/lib/llm-providers/useLlmProviders'
32+
import { useOAuthStatus } from '@/lib/llm-providers/useOAuthStatus'
33+
import { track } from '@/lib/metrics/track'
2434
import { ConfiguredProvidersList } from './ConfiguredProvidersList'
2535
import {
2636
DeleteRemoteLlmProviderDocument,
@@ -101,12 +111,69 @@ export const AISettingsPage: FC = () => {
101111
null,
102112
)
103113

114+
// OAuth status for ChatGPT Pro
115+
const {
116+
status: chatgptProStatus,
117+
startPolling: startChatGPTProPolling,
118+
disconnect: disconnectChatGPTPro,
119+
} = useOAuthStatus('chatgpt-pro')
120+
121+
// Track whether user explicitly started an OAuth flow this session
122+
const oauthFlowStartedRef = useRef(false)
123+
124+
// Auto-create provider only when user actively completed OAuth,
125+
// not on passive page load when server has old tokens
126+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — only trigger on auth status change
127+
useEffect(() => {
128+
if (!chatgptProStatus?.authenticated) return
129+
if (!oauthFlowStartedRef.current) return
130+
131+
const exists = providers.some((p) => p.type === 'chatgpt-pro')
132+
if (exists) return
133+
134+
const now = Date.now()
135+
try {
136+
const template = getProviderTemplate('chatgpt-pro')
137+
saveProvider({
138+
id: `chatgpt-pro-${now}`,
139+
type: 'chatgpt-pro',
140+
name: `ChatGPT Pro${chatgptProStatus.email ? ` (${chatgptProStatus.email})` : ''}`,
141+
modelId: template?.defaultModelId ?? 'gpt-5.3-codex',
142+
supportsImages: template?.supportsImages ?? true,
143+
contextWindow: template?.contextWindow ?? 400000,
144+
temperature: 0.2,
145+
createdAt: now,
146+
updatedAt: now,
147+
})
148+
track(CHATGPT_PRO_OAUTH_COMPLETED_EVENT, {
149+
email: chatgptProStatus.email,
150+
})
151+
toast.success('ChatGPT Pro Connected', {
152+
description: chatgptProStatus.email
153+
? `Authenticated as ${chatgptProStatus.email}`
154+
: 'Successfully authenticated with ChatGPT Pro',
155+
})
156+
} catch (err) {
157+
toast.error('Failed to create ChatGPT Pro provider', {
158+
description: err instanceof Error ? err.message : 'Unknown error',
159+
})
160+
} finally {
161+
oauthFlowStartedRef.current = false
162+
}
163+
}, [chatgptProStatus?.authenticated])
164+
104165
const handleAddProvider = () => {
105166
setTemplateValues(undefined)
106167
setIsNewDialogOpen(true)
107168
}
108169

109170
const handleUseTemplate = (template: ProviderTemplate) => {
171+
// OAuth providers: trigger OAuth flow instead of opening form dialog
172+
if (template.id === 'chatgpt-pro') {
173+
handleStartChatGPTProOAuth()
174+
return
175+
}
176+
110177
setTemplateValues({
111178
type: template.id,
112179
name: template.name,
@@ -119,6 +186,27 @@ export const AISettingsPage: FC = () => {
119186
setIsNewDialogOpen(true)
120187
}
121188

189+
const handleStartChatGPTProOAuth = () => {
190+
if (!agentServerUrl) {
191+
toast.error('Server not available', {
192+
description: 'Cannot start OAuth flow without server connection.',
193+
})
194+
return
195+
}
196+
oauthFlowStartedRef.current = true
197+
198+
const extensionSettingsUrl = chrome.runtime.getURL('app.html#/ai-settings')
199+
const startUrl = `${agentServerUrl}/oauth/chatgpt-pro/start?redirect=${encodeURIComponent(extensionSettingsUrl)}`
200+
window.open(startUrl, '_blank')
201+
202+
// Start polling for OAuth completion
203+
startChatGPTProPolling()
204+
track(CHATGPT_PRO_OAUTH_STARTED_EVENT)
205+
toast.info('Authenticating with ChatGPT Pro', {
206+
description: 'Complete the login in the opened tab.',
207+
})
208+
}
209+
122210
const handleEditProvider = (provider: LlmProviderConfig) => {
123211
setEditingProvider(provider)
124212
setIsEditDialogOpen(true)
@@ -130,6 +218,11 @@ export const AISettingsPage: FC = () => {
130218

131219
const confirmDeleteProvider = async () => {
132220
if (providerToDelete) {
221+
// Clear OAuth tokens on server for OAuth-based providers
222+
if (providerToDelete.type === 'chatgpt-pro') {
223+
await disconnectChatGPTPro()
224+
track(CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT)
225+
}
133226
await deleteProvider(providerToDelete.id)
134227
deleteRemoteProviderMutation.mutate({ rowId: providerToDelete.id })
135228
setProviderToDelete(null)

packages/browseros-agent/apps/agent/entrypoints/app/ai-settings/NewProviderDialog.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const providerTypeEnum = z.enum([
6161
'lmstudio',
6262
'bedrock',
6363
'browseros',
64+
'chatgpt-pro',
6465
])
6566

6667
/**
@@ -84,6 +85,9 @@ export const providerFormSchema = z
8485
secretAccessKey: z.string().optional(),
8586
region: z.string().optional(),
8687
sessionToken: z.string().optional(),
88+
// ChatGPT Pro (Codex)
89+
reasoningEffort: z.enum(['none', 'low', 'medium', 'high']).optional(),
90+
reasoningSummary: z.enum(['auto', 'concise', 'detailed']).optional(),
8791
})
8892
.superRefine((data, ctx) => {
8993
// Azure: require either resourceName or baseUrl
@@ -127,6 +131,10 @@ export const providerFormSchema = z
127131
})
128132
}
129133
}
134+
// ChatGPT Pro: no credentials needed (server-managed OAuth)
135+
else if (data.type === 'chatgpt-pro') {
136+
// No validation needed — OAuth tokens are on the server
137+
}
130138
// Other providers: require baseUrl
131139
else if (!data.baseUrl) {
132140
ctx.addIssue({
@@ -209,6 +217,8 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
209217
secretAccessKey: initialValues?.secretAccessKey || '',
210218
region: initialValues?.region || '',
211219
sessionToken: initialValues?.sessionToken || '',
220+
reasoningEffort: initialValues?.reasoningEffort || 'high',
221+
reasoningSummary: initialValues?.reasoningSummary || 'auto',
212222
},
213223
})
214224

@@ -301,6 +311,8 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
301311
secretAccessKey: initialValues.secretAccessKey || '',
302312
region: initialValues.region || '',
303313
sessionToken: initialValues.sessionToken || '',
314+
reasoningEffort: initialValues.reasoningEffort || 'high',
315+
reasoningSummary: initialValues.reasoningSummary || 'auto',
304316
})
305317
setIsCustomModel(false)
306318
}
@@ -326,6 +338,8 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
326338
secretAccessKey: '',
327339
region: '',
328340
sessionToken: '',
341+
reasoningEffort: 'high',
342+
reasoningSummary: 'auto',
329343
})
330344
setIsCustomModel(false)
331345
}
@@ -363,6 +377,9 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
363377
const canTest = (): boolean => {
364378
if (!watchedModelId) return false
365379

380+
// ChatGPT Pro: always testable (server has the OAuth token)
381+
if (watchedType === 'chatgpt-pro') return true
382+
366383
if (watchedType === 'azure') {
367384
return !!(watchedResourceName || watchedBaseUrl) && !!watchedApiKey
368385
}
@@ -444,6 +461,76 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
444461
}
445462

446463
const renderProviderSpecificFields = () => {
464+
// ChatGPT Pro: OAuth credentials + Codex reasoning settings
465+
if (watchedType === 'chatgpt-pro') {
466+
return (
467+
<>
468+
<div className="rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 text-sm dark:border-green-800 dark:bg-green-950 dark:text-green-300">
469+
Credentials are managed via OAuth. No API key needed.
470+
</div>
471+
<div className="grid gap-4 sm:grid-cols-2">
472+
<FormField
473+
control={form.control}
474+
name="reasoningEffort"
475+
render={({ field }) => (
476+
<FormItem>
477+
<FormLabel>Reasoning Effort</FormLabel>
478+
<Select
479+
onValueChange={field.onChange}
480+
value={field.value || 'high'}
481+
>
482+
<FormControl>
483+
<SelectTrigger className="w-full">
484+
<SelectValue />
485+
</SelectTrigger>
486+
</FormControl>
487+
<SelectContent>
488+
<SelectItem value="none">None</SelectItem>
489+
<SelectItem value="low">Low</SelectItem>
490+
<SelectItem value="medium">Medium</SelectItem>
491+
<SelectItem value="high">High</SelectItem>
492+
</SelectContent>
493+
</Select>
494+
<FormDescription>
495+
How much the model thinks before responding
496+
</FormDescription>
497+
<FormMessage />
498+
</FormItem>
499+
)}
500+
/>
501+
<FormField
502+
control={form.control}
503+
name="reasoningSummary"
504+
render={({ field }) => (
505+
<FormItem>
506+
<FormLabel>Reasoning Summary</FormLabel>
507+
<Select
508+
onValueChange={field.onChange}
509+
value={field.value || 'auto'}
510+
>
511+
<FormControl>
512+
<SelectTrigger className="w-full">
513+
<SelectValue />
514+
</SelectTrigger>
515+
</FormControl>
516+
<SelectContent>
517+
<SelectItem value="auto">Auto</SelectItem>
518+
<SelectItem value="concise">Concise</SelectItem>
519+
<SelectItem value="detailed">Detailed</SelectItem>
520+
</SelectContent>
521+
</Select>
522+
<FormDescription>
523+
Detail level of visible thinking steps
524+
</FormDescription>
525+
<FormMessage />
526+
</FormItem>
527+
)}
528+
/>
529+
</div>
530+
</>
531+
)
532+
}
533+
447534
if (watchedType === 'azure') {
448535
return (
449536
<>

packages/browseros-agent/apps/agent/entrypoints/app/ai-settings/models.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ModelsData {
2323
bedrock: ModelInfo[]
2424
browseros: ModelInfo[]
2525
moonshot: ModelInfo[]
26+
'chatgpt-pro': ModelInfo[]
2627
}
2728

2829
/**
@@ -90,6 +91,16 @@ export const MODELS_DATA: ModelsData = {
9091
],
9192
bedrock: [],
9293
browseros: [{ modelId: 'browseros-auto', contextLength: 200000 }],
94+
'chatgpt-pro': [
95+
{ modelId: 'gpt-5.4', contextLength: 400000 },
96+
{ modelId: 'gpt-5.3-codex', contextLength: 400000 },
97+
{ modelId: 'gpt-5.2-codex', contextLength: 400000 },
98+
{ modelId: 'gpt-5.2', contextLength: 200000 },
99+
{ modelId: 'gpt-5.1-codex', contextLength: 400000 },
100+
{ modelId: 'gpt-5.1-codex-max', contextLength: 400000 },
101+
{ modelId: 'gpt-5.1-codex-mini', contextLength: 400000 },
102+
{ modelId: 'gpt-5.1', contextLength: 200000 },
103+
],
93104
}
94105

95106
/**

packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/useChatSession.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,9 @@ export const useChatSession = (options?: ChatSessionOptions) => {
308308
secretAccessKey: provider?.secretAccessKey,
309309
region: provider?.region,
310310
sessionToken: provider?.sessionToken,
311+
// ChatGPT Pro (Codex)
312+
reasoningEffort: provider?.reasoningEffort,
313+
reasoningSummary: provider?.reasoningSummary,
311314
browserContext,
312315
userSystemPrompt:
313316
options?.origin === 'newtab'

packages/browseros-agent/apps/agent/lib/constants/analyticsEvents.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ export const CONVERSATION_RESET_EVENT = 'ui.conversation.reset'
2929
/** @public */
3030
export const AI_PROVIDER_ADDED_EVENT = 'settings.ai_provider.added'
3131

32+
/** @public */
33+
export const CHATGPT_PRO_OAUTH_STARTED_EVENT =
34+
'settings.chatgpt_pro.oauth_started'
35+
36+
/** @public */
37+
export const CHATGPT_PRO_OAUTH_COMPLETED_EVENT =
38+
'settings.chatgpt_pro.oauth_completed'
39+
40+
/** @public */
41+
export const CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT =
42+
'settings.chatgpt_pro.oauth_disconnected'
43+
3244
/** @public */
3345
export const HUB_PROVIDER_ADDED_EVENT = 'settings.hub_provider.added'
3446

packages/browseros-agent/apps/agent/lib/llm-providers/providerTemplates.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ export interface ProviderTemplate {
2020
* @public
2121
*/
2222
export const providerTemplates: ProviderTemplate[] = [
23+
{
24+
id: 'chatgpt-pro',
25+
name: 'ChatGPT Pro',
26+
defaultBaseUrl: 'https://chatgpt.com/backend-api',
27+
defaultModelId: 'gpt-5.3-codex',
28+
supportsImages: true,
29+
contextWindow: 400000,
30+
setupGuideUrl: 'https://docs.browseros.com/features/chatgpt-pro-oauth',
31+
},
2332
{
2433
id: 'moonshot',
2534
name: 'Moonshot AI',
@@ -129,6 +138,7 @@ export const providerTemplates: ProviderTemplate[] = [
129138
* @public
130139
*/
131140
export const providerTypeOptions: { value: ProviderType; label: string }[] = [
141+
{ value: 'chatgpt-pro', label: 'ChatGPT Pro' },
132142
{ value: 'moonshot', label: 'Moonshot AI' },
133143
{ value: 'anthropic', label: 'Anthropic' },
134144
{ value: 'openai', label: 'OpenAI' },
@@ -157,6 +167,7 @@ export const getProviderTemplate = (
157167
* Auto-fills when user selects a provider type
158168
*/
159169
export const DEFAULT_BASE_URLS: Record<ProviderType, string> = {
170+
'chatgpt-pro': 'https://chatgpt.com/backend-api',
160171
moonshot: 'https://api.moonshot.ai/v1',
161172
anthropic: 'https://api.anthropic.com/v1',
162173
openai: 'https://api.openai.com/v1',

packages/browseros-agent/apps/agent/lib/llm-providers/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type ProviderType =
1414
| 'bedrock'
1515
| 'browseros'
1616
| 'moonshot'
17+
| 'chatgpt-pro'
1718

1819
/**
1920
* LLM Provider configuration
@@ -56,6 +57,10 @@ export interface LlmProviderConfig {
5657
region?: string
5758
/** AWS session token (for temporary STS credentials) */
5859
sessionToken?: string
60+
61+
// ChatGPT Pro (Codex) fields
62+
reasoningEffort?: 'none' | 'low' | 'medium' | 'high'
63+
reasoningSummary?: 'auto' | 'concise' | 'detailed'
5964
}
6065

6166
/**

0 commit comments

Comments
 (0)