Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
fdd1dc9
feat: add ChatGPT Pro OAuth as LLM provider
shivammittal274 Mar 17, 2026
cd717f1
Merge branch 'main' into feat/chatgpt-pro-oauth
shivammittal274 Mar 17, 2026
007538d
fix: address Greptile PR review comments
shivammittal274 Mar 17, 2026
349523d
simplify: remove unnecessary OAuth shutdown wiring and useCallback
shivammittal274 Mar 17, 2026
278352f
style: add readonly modifiers and braces per TS style guide
shivammittal274 Mar 17, 2026
5d8c61f
docs: add E2E test screenshots for ChatGPT Pro OAuth
shivammittal274 Mar 18, 2026
44affdf
fix: strip item IDs from Codex requests to fix multi-turn conversations
shivammittal274 Mar 18, 2026
67007f1
fix: preserve function_call_output IDs in Codex requests
shivammittal274 Mar 18, 2026
e00f3b3
fix: resolve Codex store=false + tool-use incompatibility
shivammittal274 Mar 18, 2026
99b8c23
fix: remove non-Codex model gpt-5.2 from chatgpt-pro model list
shivammittal274 Mar 18, 2026
ca28ed5
fix: strip unsupported Codex params and update model list
shivammittal274 Mar 18, 2026
65760ab
chore: remove screenshots containing email
shivammittal274 Mar 18, 2026
64e6a97
feat: enable reasoning events for ChatGPT Pro Codex models
shivammittal274 Mar 18, 2026
4788c82
chore: set reasoning effort to high for ChatGPT Pro
shivammittal274 Mar 18, 2026
557a476
feat: add configurable reasoning effort and summary for ChatGPT Pro
shivammittal274 Mar 18, 2026
6b592bb
fix: strip max_output_tokens from Codex requests (fixes compaction)
shivammittal274 Mar 18, 2026
f81091f
fix: address Greptile P1 issues
shivammittal274 Mar 18, 2026
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQueryClient } from '@tanstack/react-query'
import { type FC, useMemo, useState } from 'react'
import { type FC, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
AlertDialog,
Expand All @@ -13,6 +13,11 @@ import {
} from '@/components/ui/alert-dialog'
import { useSessionInfo } from '@/lib/auth/sessionStorage'
import { useAgentServerUrl } from '@/lib/browseros/useBrowserOSProviders'
import {
CHATGPT_PRO_OAUTH_COMPLETED_EVENT,
CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT,
CHATGPT_PRO_OAUTH_STARTED_EVENT,
} from '@/lib/constants/analyticsEvents'
import { GetProfileIdByUserIdDocument } from '@/lib/conversations/graphql/uploadConversationDocument'
import { getQueryKeyFromDocument } from '@/lib/graphql/getQueryKeyFromDocument'
import { useGraphqlMutation } from '@/lib/graphql/useGraphqlMutation'
Expand All @@ -21,6 +26,8 @@ import type { ProviderTemplate } from '@/lib/llm-providers/providerTemplates'
import { testProvider } from '@/lib/llm-providers/testProvider'
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
import { useLlmProviders } from '@/lib/llm-providers/useLlmProviders'
import { useOAuthStatus } from '@/lib/llm-providers/useOAuthStatus'
import { track } from '@/lib/metrics/track'
import { ConfiguredProvidersList } from './ConfiguredProvidersList'
import {
DeleteRemoteLlmProviderDocument,
Expand Down Expand Up @@ -101,12 +108,65 @@ export const AISettingsPage: FC = () => {
null,
)

// OAuth status for ChatGPT Pro
const {
status: chatgptProStatus,
startPolling: startChatGPTProPolling,
disconnect: disconnectChatGPTPro,
} = useOAuthStatus('chatgpt-pro')

// Guard against duplicate auto-create calls
const isCreatingOAuthProviderRef = useRef(false)

// Auto-create provider when OAuth completes
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — only trigger on auth status change
useEffect(() => {
if (!chatgptProStatus?.authenticated) return
if (isCreatingOAuthProviderRef.current) return

const exists = providers.some((p) => p.type === 'chatgpt-pro')
if (exists) return

isCreatingOAuthProviderRef.current = true

const now = Date.now()
try {
saveProvider({
id: `chatgpt-pro-${now}`,
type: 'chatgpt-pro',
name: `ChatGPT Pro${chatgptProStatus.email ? ` (${chatgptProStatus.email})` : ''}`,
modelId: 'gpt-5.3-codex',
supportsImages: true,
contextWindow: 400000,
temperature: 0.2,
createdAt: now,
updatedAt: now,
})
track(CHATGPT_PRO_OAUTH_COMPLETED_EVENT, {
email: chatgptProStatus.email,
})
toast.success('ChatGPT Pro Connected', {
description: chatgptProStatus.email
? `Authenticated as ${chatgptProStatus.email}`
: 'Successfully authenticated with ChatGPT Pro',
})
} finally {
isCreatingOAuthProviderRef.current = false
}
}, [chatgptProStatus?.authenticated])

const handleAddProvider = () => {
setTemplateValues(undefined)
setIsNewDialogOpen(true)
}

const handleUseTemplate = (template: ProviderTemplate) => {
// OAuth providers: trigger OAuth flow instead of opening form dialog
if (template.id === 'chatgpt-pro') {
handleStartChatGPTProOAuth()
return
}

setTemplateValues({
type: template.id,
name: template.name,
Expand All @@ -119,6 +179,26 @@ export const AISettingsPage: FC = () => {
setIsNewDialogOpen(true)
}

const handleStartChatGPTProOAuth = () => {
if (!agentServerUrl) {
toast.error('Server not available', {
description: 'Cannot start OAuth flow without server connection.',
})
return
}

const extensionSettingsUrl = chrome.runtime.getURL('app.html#/ai-settings')
const startUrl = `${agentServerUrl}/oauth/chatgpt-pro/start?redirect=${encodeURIComponent(extensionSettingsUrl)}`
window.open(startUrl, '_blank')

// Start polling for OAuth completion
startChatGPTProPolling()
track(CHATGPT_PRO_OAUTH_STARTED_EVENT)
toast.info('Authenticating with ChatGPT Pro', {
description: 'Complete the login in the opened tab.',
})
}

const handleEditProvider = (provider: LlmProviderConfig) => {
setEditingProvider(provider)
setIsEditDialogOpen(true)
Expand All @@ -130,6 +210,11 @@ export const AISettingsPage: FC = () => {

const confirmDeleteProvider = async () => {
if (providerToDelete) {
// Clear OAuth tokens on server for OAuth-based providers
if (providerToDelete.type === 'chatgpt-pro') {
await disconnectChatGPTPro()
track(CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT)
}
await deleteProvider(providerToDelete.id)
deleteRemoteProviderMutation.mutate({ rowId: providerToDelete.id })
setProviderToDelete(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const providerTypeEnum = z.enum([
'lmstudio',
'bedrock',
'browseros',
'chatgpt-pro',
])

/**
Expand Down Expand Up @@ -127,6 +128,10 @@ export const providerFormSchema = z
})
}
}
// ChatGPT Pro: no credentials needed (server-managed OAuth)
else if (data.type === 'chatgpt-pro') {
// No validation needed — OAuth tokens are on the server
}
// Other providers: require baseUrl
else if (!data.baseUrl) {
ctx.addIssue({
Expand Down Expand Up @@ -363,6 +368,9 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
const canTest = (): boolean => {
if (!watchedModelId) return false

// ChatGPT Pro: always testable (server has the OAuth token)
if (watchedType === 'chatgpt-pro') return true

if (watchedType === 'azure') {
return !!(watchedResourceName || watchedBaseUrl) && !!watchedApiKey
}
Expand Down Expand Up @@ -444,6 +452,15 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
}

const renderProviderSpecificFields = () => {
// ChatGPT Pro: no credential fields (server manages OAuth tokens)
if (watchedType === 'chatgpt-pro') {
return (
<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">
Credentials are managed via OAuth. No API key needed.
</div>
)
}

if (watchedType === 'azure') {
return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface ModelsData {
bedrock: ModelInfo[]
browseros: ModelInfo[]
moonshot: ModelInfo[]
'chatgpt-pro': ModelInfo[]
}

/**
Expand Down Expand Up @@ -90,6 +91,14 @@ export const MODELS_DATA: ModelsData = {
],
bedrock: [],
browseros: [{ modelId: 'browseros-auto', contextLength: 200000 }],
'chatgpt-pro': [
{ modelId: 'gpt-5.3-codex', contextLength: 400000 },
{ modelId: 'gpt-5.2-codex', contextLength: 400000 },
{ modelId: 'gpt-5.2', contextLength: 200000 },
{ modelId: 'gpt-5.1-codex', contextLength: 400000 },
{ modelId: 'gpt-5.1-codex-max', contextLength: 400000 },
{ modelId: 'gpt-5.1-codex-mini', contextLength: 400000 },
],
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export const CONVERSATION_RESET_EVENT = 'ui.conversation.reset'
/** @public */
export const AI_PROVIDER_ADDED_EVENT = 'settings.ai_provider.added'

/** @public */
export const CHATGPT_PRO_OAUTH_STARTED_EVENT =
'settings.chatgpt_pro.oauth_started'

/** @public */
export const CHATGPT_PRO_OAUTH_COMPLETED_EVENT =
'settings.chatgpt_pro.oauth_completed'

/** @public */
export const CHATGPT_PRO_OAUTH_DISCONNECTED_EVENT =
'settings.chatgpt_pro.oauth_disconnected'

/** @public */
export const HUB_PROVIDER_ADDED_EVENT = 'settings.hub_provider.added'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export interface ProviderTemplate {
* @public
*/
export const providerTemplates: ProviderTemplate[] = [
{
id: 'chatgpt-pro',
name: 'ChatGPT Pro',
defaultBaseUrl: 'https://chatgpt.com/backend-api',
defaultModelId: 'gpt-5.3-codex',
supportsImages: true,
contextWindow: 400000,
setupGuideUrl: 'https://docs.browseros.com/features/chatgpt-pro-oauth',
},
{
id: 'moonshot',
name: 'Moonshot AI',
Expand Down Expand Up @@ -129,6 +138,7 @@ export const providerTemplates: ProviderTemplate[] = [
* @public
*/
export const providerTypeOptions: { value: ProviderType; label: string }[] = [
{ value: 'chatgpt-pro', label: 'ChatGPT Pro' },
{ value: 'moonshot', label: 'Moonshot AI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
Expand Down Expand Up @@ -157,6 +167,7 @@ export const getProviderTemplate = (
* Auto-fills when user selects a provider type
*/
export const DEFAULT_BASE_URLS: Record<ProviderType, string> = {
'chatgpt-pro': 'https://chatgpt.com/backend-api',
moonshot: 'https://api.moonshot.ai/v1',
anthropic: 'https://api.anthropic.com/v1',
openai: 'https://api.openai.com/v1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type ProviderType =
| 'bedrock'
| 'browseros'
| 'moonshot'
| 'chatgpt-pro'

/**
* LLM Provider configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { getAgentServerUrl } from '@/lib/browseros/helpers'

interface OAuthStatus {
authenticated: boolean
email?: string
provider: string
}

interface UseOAuthStatusReturn {
status: OAuthStatus | null
isPolling: boolean
startPolling: () => void
stopPolling: () => void
refresh: () => Promise<OAuthStatus | null>
disconnect: () => Promise<void>
}

export function useOAuthStatus(provider: string): UseOAuthStatusReturn {
const [status, setStatus] = useState<OAuthStatus | null>(null)
const [isPolling, setIsPolling] = useState(false)
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const fetchStatus = useCallback(async (): Promise<OAuthStatus | null> => {
try {
const serverUrl = await getAgentServerUrl()
const res = await fetch(`${serverUrl}/oauth/${provider}/status`)
if (!res.ok) return null
const data = (await res.json()) as OAuthStatus
setStatus(data)
return data
} catch {
return null
}
}, [provider])

// Initial status check on mount
useEffect(() => {
fetchStatus()
}, [fetchStatus])

const stopPolling = useCallback(() => {
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current)
if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current)
pollIntervalRef.current = null
pollTimeoutRef.current = null
setIsPolling(false)
}, [])

const startPolling = useCallback(() => {
stopPolling()
setIsPolling(true)

// Poll for status every 2 seconds
pollIntervalRef.current = setInterval(async () => {
const result = await fetchStatus()
if (result?.authenticated) {
stopPolling()
}
}, 2_000)

// Stop polling after 5 minutes
pollTimeoutRef.current = setTimeout(stopPolling, 300_000)
}, [fetchStatus, stopPolling])

const disconnect = useCallback(async () => {
try {
const serverUrl = await getAgentServerUrl()
await fetch(`${serverUrl}/oauth/${provider}`, { method: 'DELETE' })
setStatus({ authenticated: false, provider })
} catch {
// Best-effort disconnect
}
}, [provider])

// Cleanup on unmount
useEffect(() => stopPolling, [stopPolling])

return {
status,
isPolling,
startPolling,
stopPolling,
refresh: fetchStatus,
disconnect,
}
}
13 changes: 13 additions & 0 deletions packages/browseros-agent/apps/server/src/agent/provider-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import { LLM_PROVIDERS } from '@browseros/shared/schemas/llm'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import type { LanguageModel } from 'ai'
import { createCodexFetch } from '../lib/clients/oauth/codex-fetch'
import { logger } from '../lib/logger'
import { createOpenRouterCompatibleFetch } from '../lib/openrouter-fetch'
import type { ResolvedAgentConfig } from './types'
Expand Down Expand Up @@ -148,6 +149,17 @@ function createMoonshotFactory(
})
}

function createChatGPTProFactory(
config: ResolvedAgentConfig,
): (modelId: string) => unknown {
if (!config.apiKey)
throw new Error('ChatGPT Pro requires OAuth authentication')
return createOpenAI({
apiKey: config.apiKey,
fetch: createCodexFetch(config.accountId) as typeof globalThis.fetch,
}).responses
}

const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.ANTHROPIC]: createAnthropicFactory,
[LLM_PROVIDERS.OPENAI]: createOpenAIFactory,
Expand All @@ -160,6 +172,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.BROWSEROS]: createBrowserOSFactory,
[LLM_PROVIDERS.OPENAI_COMPATIBLE]: createOpenAICompatibleFactory,
[LLM_PROVIDERS.MOONSHOT]: createMoonshotFactory,
[LLM_PROVIDERS.CHATGPT_PRO]: createChatGPTProFactory,
}

export function createLanguageModel(
Expand Down
1 change: 1 addition & 0 deletions packages/browseros-agent/apps/server/src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ResolvedAgentConfig {
accessKeyId?: string
secretAccessKey?: string
sessionToken?: string
accountId?: string
contextWindowSize?: number
userSystemPrompt?: string
workingDir: string
Expand Down
Loading
Loading