mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Merge pull request #438 from q1352013520/feature/codex-subscription
feat: /login支持codex订阅登录
This commit is contained in:
26
src/buddy/__tests__/companion.test.ts
Normal file
26
src/buddy/__tests__/companion.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { inferLegacyCompanionBones } from '../companion.js'
|
||||
|
||||
describe('inferLegacyCompanionBones', () => {
|
||||
test('infers species and rarity from legacy seedless companion text', () => {
|
||||
expect(
|
||||
inferLegacyCompanionBones({
|
||||
name: 'Biscuit',
|
||||
personality: 'A common mushroom of few words.',
|
||||
}),
|
||||
).toEqual({
|
||||
species: 'mushroom',
|
||||
rarity: 'common',
|
||||
})
|
||||
})
|
||||
|
||||
test('does not override seeded companions', () => {
|
||||
expect(
|
||||
inferLegacyCompanionBones({
|
||||
name: 'Spore',
|
||||
personality: 'A common mushroom of few words.',
|
||||
seed: 'rehatch-1',
|
||||
}),
|
||||
).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { getGlobalConfig } from '../utils/config.js'
|
||||
import {
|
||||
type Companion,
|
||||
type CompanionBones,
|
||||
type CompanionSoul,
|
||||
EYES,
|
||||
HATS,
|
||||
RARITIES,
|
||||
@@ -125,12 +126,36 @@ export function companionUserId(): string {
|
||||
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
|
||||
}
|
||||
|
||||
const WORD_BOUNDARY = '[^a-z0-9]+'
|
||||
|
||||
function hasWord(text: string, word: string): boolean {
|
||||
return new RegExp(`(^|${WORD_BOUNDARY})${word}($|${WORD_BOUNDARY})`).test(
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
export function inferLegacyCompanionBones(
|
||||
stored: CompanionSoul,
|
||||
): Partial<Pick<CompanionBones, 'species' | 'rarity'>> {
|
||||
if (stored.seed) return {}
|
||||
const text = `${stored.name} ${stored.personality}`.toLowerCase()
|
||||
const inferred: Partial<Pick<CompanionBones, 'species' | 'rarity'>> = {}
|
||||
const species = SPECIES.find(species => hasWord(text, species))
|
||||
const rarity = RARITIES.find(rarity => hasWord(text, rarity))
|
||||
if (species) inferred.species = species
|
||||
if (rarity) inferred.rarity = rarity
|
||||
return inferred
|
||||
}
|
||||
|
||||
// Regenerate bones from seed or userId, merge with stored soul.
|
||||
export function getCompanion(): Companion | undefined {
|
||||
const stored = getGlobalConfig().companion
|
||||
if (!stored) return undefined
|
||||
const seed = stored.seed ?? companionUserId()
|
||||
const { bones } = rollWithSeed(seed)
|
||||
// bones last so stale bones fields in old-format configs get overridden
|
||||
return { ...stored, ...bones }
|
||||
const legacyBones = inferLegacyCompanionBones(stored)
|
||||
// Seeded companions use regenerated bones. Legacy seedless companions may
|
||||
// have species/rarity embedded in their generated soul text; keep that
|
||||
// visible identity coherent when the userId-derived roll drifts.
|
||||
return { ...stored, ...bones, ...legacyBones }
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg
|
||||
|
||||
if (COMMON_HELP_ARGS.includes(args)) {
|
||||
onDone(
|
||||
'Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6/4.7, DeepSeek V4 Pro)\n- auto: Use the default effort level for your model',
|
||||
'Usage: /effort [low|medium|high|xhigh|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- xhigh: Extra high reasoning for supported models, including ChatGPT Codex models\n- max: Maximum capability with deepest reasoning where supported (Opus 4.6/4.7, DeepSeek V4 Pro); maps to xhigh for ChatGPT Codex models\n- auto: Use the default effort level for your model',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
export default {
|
||||
type: 'local-jsx',
|
||||
name: 'logout',
|
||||
description: 'Sign out from your Anthropic account',
|
||||
description: 'Sign out from your configured account',
|
||||
isEnabled: () => !isEnvTruthy(process.env.DISABLE_LOGOUT_COMMAND),
|
||||
load: () => import('./logout.js'),
|
||||
} satisfies Command
|
||||
|
||||
@@ -6,11 +6,13 @@ import { getGroveNoticeConfig, getGroveSettings } from '../../services/api/grove
|
||||
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js';
|
||||
// flushTelemetry is loaded lazily to avoid pulling in ~1.1MB of OpenTelemetry at startup
|
||||
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js';
|
||||
import { removeChatGPTAuth } from '../../services/api/openai/chatgptAuth.js';
|
||||
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js';
|
||||
import { clearBetasCaches } from '../../utils/betas.js';
|
||||
import { saveGlobalConfig } from '../../utils/config.js';
|
||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js';
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js';
|
||||
import { getSettingsForSource, updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js';
|
||||
import { resetUserCache } from '../../utils/user.js';
|
||||
|
||||
@@ -20,6 +22,8 @@ export async function performLogout({ clearOnboarding = false }): Promise<void>
|
||||
await flushTelemetry();
|
||||
|
||||
await removeApiKey();
|
||||
await removeChatGPTAuth();
|
||||
clearChatGPTSettingsAuthMode();
|
||||
|
||||
// Wipe all secure storage data on logout
|
||||
const secureStorage = getSecureStorage();
|
||||
@@ -44,6 +48,22 @@ export async function performLogout({ clearOnboarding = false }): Promise<void>
|
||||
});
|
||||
}
|
||||
|
||||
function clearChatGPTSettingsAuthMode(): void {
|
||||
delete process.env.OPENAI_AUTH_MODE;
|
||||
const userSettings = getSettingsForSource('userSettings') ?? {};
|
||||
const env = userSettings.env ?? {};
|
||||
const hasOpenAICompatibleConfig =
|
||||
Boolean(env.OPENAI_API_KEY ?? process.env.OPENAI_API_KEY) &&
|
||||
Boolean(env.OPENAI_BASE_URL ?? process.env.OPENAI_BASE_URL);
|
||||
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||
...(userSettings.modelType === 'openai' && !hasOpenAICompatibleConfig ? { modelType: undefined } : {}),
|
||||
env: {
|
||||
OPENAI_AUTH_MODE: undefined,
|
||||
} as unknown as Record<string, string>,
|
||||
};
|
||||
updateSettingsForSource('userSettings', settingsUpdate);
|
||||
}
|
||||
|
||||
// clearing anything memoized that must be invalidated when user/session/auth changes
|
||||
export async function clearAuthRelatedCaches(): Promise<void> {
|
||||
// Clear the OAuth token cache
|
||||
@@ -70,7 +90,7 @@ export async function clearAuthRelatedCaches(): Promise<void> {
|
||||
export async function call(): Promise<React.ReactNode> {
|
||||
await performLogout({ clearOnboarding: true });
|
||||
|
||||
const message = <Text>Successfully logged out from your Anthropic account.</Text>;
|
||||
const message = <Text>Successfully logged out.</Text>;
|
||||
|
||||
setTimeout(() => {
|
||||
gracefulShutdownSync(0, 'logout');
|
||||
|
||||
@@ -81,9 +81,10 @@ const call: LocalCommandCall = async (args, _context) => {
|
||||
// Check env vars when switching to openai (including settings.env)
|
||||
if (arg === 'openai') {
|
||||
const mergedEnv = getMergedEnv()
|
||||
const hasChatGPTAuth = mergedEnv.OPENAI_AUTH_MODE === 'chatgpt'
|
||||
const hasKey = !!mergedEnv.OPENAI_API_KEY
|
||||
const hasUrl = !!mergedEnv.OPENAI_BASE_URL
|
||||
if (!hasKey || !hasUrl) {
|
||||
if (!hasChatGPTAuth && (!hasKey || !hasUrl)) {
|
||||
updateSettingsForSource('userSettings', { modelType: 'openai' })
|
||||
const missing = []
|
||||
if (!hasKey) missing.push('OPENAI_API_KEY')
|
||||
|
||||
@@ -9,9 +9,14 @@ import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcu
|
||||
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
||||
import { getSSLErrorHint } from '@ant/model-provider';
|
||||
import { sendNotification } from '../services/notifier.js';
|
||||
import {
|
||||
completeChatGPTDeviceLogin,
|
||||
requestChatGPTDeviceCode,
|
||||
type ChatGPTDeviceCode,
|
||||
} from '../services/api/openai/chatgptAuth.js';
|
||||
import { OAuthService } from '../services/oauth/index.js';
|
||||
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
|
||||
|
||||
import { openBrowser } from '../utils/browser.js';
|
||||
import { logError } from '../utils/log.js';
|
||||
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { Select } from './CustomSelect/select.js';
|
||||
@@ -46,6 +51,11 @@ type OAuthStatus =
|
||||
opusModel: string;
|
||||
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||
} // OpenAI Chat Completions API platform
|
||||
| {
|
||||
state: 'chatgpt_subscription';
|
||||
phase: 'requesting' | 'waiting';
|
||||
deviceCode?: ChatGPTDeviceCode;
|
||||
} // ChatGPT account subscription via Codex OAuth device flow
|
||||
| {
|
||||
state: 'gemini_api';
|
||||
baseUrl: string;
|
||||
@@ -445,6 +455,15 @@ function OAuthStatusMessage({
|
||||
),
|
||||
value: 'openai_chat_api',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
ChatGPT account with subscription · <Text dimColor>Plus, Pro, Business, Edu, or Enterprise</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'chatgpt_subscription',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
@@ -515,6 +534,12 @@ function OAuthStatusMessage({
|
||||
opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '',
|
||||
activeField: 'base_url',
|
||||
});
|
||||
} else if (value === 'chatgpt_subscription') {
|
||||
logEvent('tengu_chatgpt_subscription_selected', {});
|
||||
setOAuthStatus({
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'requesting',
|
||||
});
|
||||
} else if (value === 'gemini_api') {
|
||||
logEvent('tengu_gemini_api_selected', {});
|
||||
setOAuthStatus({
|
||||
@@ -807,7 +832,9 @@ function OAuthStatusMessage({
|
||||
|
||||
const doOpenAISave = useCallback(() => {
|
||||
const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue };
|
||||
const env: Record<string, string> = {};
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENAI_AUTH_MODE: undefined,
|
||||
};
|
||||
|
||||
// Validate base_url if provided
|
||||
if (finalVals.base_url) {
|
||||
@@ -836,10 +863,11 @@ function OAuthStatusMessage({
|
||||
if (finalVals.haiku_model) env.OPENAI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model;
|
||||
if (finalVals.sonnet_model) env.OPENAI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||
if (finalVals.opus_model) env.OPENAI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
modelType: 'openai' as any,
|
||||
env,
|
||||
} as any);
|
||||
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||
modelType: 'openai',
|
||||
env: env as unknown as Record<string, string>,
|
||||
};
|
||||
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
|
||||
if (error) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
@@ -855,7 +883,13 @@ function OAuthStatusMessage({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
||||
for (const [k, v] of Object.entries(env)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k];
|
||||
} else {
|
||||
process.env[k] = v;
|
||||
}
|
||||
}
|
||||
setOAuthStatus({ state: 'success' });
|
||||
void onDone();
|
||||
}
|
||||
@@ -953,6 +987,93 @@ function OAuthStatusMessage({
|
||||
);
|
||||
}
|
||||
|
||||
case 'chatgpt_subscription': {
|
||||
const status = oauthStatus as {
|
||||
state: 'chatgpt_subscription';
|
||||
phase: 'requesting' | 'waiting';
|
||||
deviceCode?: ChatGPTDeviceCode;
|
||||
};
|
||||
const startedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (startedRef.current) return;
|
||||
startedRef.current = true;
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
async function runLogin() {
|
||||
try {
|
||||
const deviceCode = await requestChatGPTDeviceCode();
|
||||
if (cancelled) return;
|
||||
setOAuthStatus({
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'waiting',
|
||||
deviceCode,
|
||||
});
|
||||
void openBrowser(deviceCode.verificationUrl);
|
||||
await completeChatGPTDeviceLogin(deviceCode, controller.signal);
|
||||
if (cancelled) return;
|
||||
const env: Record<string, string> = {
|
||||
OPENAI_AUTH_MODE: 'chatgpt',
|
||||
};
|
||||
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||
modelType: 'openai',
|
||||
env,
|
||||
};
|
||||
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
|
||||
if (error) {
|
||||
throw new Error('Failed to save settings. Please try again.');
|
||||
}
|
||||
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
||||
setOAuthStatus({ state: 'success' });
|
||||
void onDone();
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: (err as Error).message,
|
||||
toRetry: {
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'requesting',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
void runLogin();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [setOAuthStatus, onDone]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold>ChatGPT Account Setup</Text>
|
||||
{status.phase === 'requesting' && (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Requesting sign-in code…</Text>
|
||||
</Box>
|
||||
)}
|
||||
{status.phase === 'waiting' && status.deviceCode && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>Open this link and sign in with your ChatGPT account:</Text>
|
||||
<Link url={status.deviceCode.verificationUrl}>
|
||||
<Text dimColor>{status.deviceCode.verificationUrl}</Text>
|
||||
</Link>
|
||||
<Text>
|
||||
Enter code: <Text bold>{status.deviceCode.userCode}</Text>
|
||||
</Text>
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Waiting for ChatGPT authorization…</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Text dimColor>Esc to go back. Device codes expire after 15 minutes.</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
case 'gemini_api': {
|
||||
type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||
const GEMINI_FIELDS: GeminiField[] = ['base_url', 'api_key', 'haiku_model', 'sonnet_model', 'opus_model'];
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getDefaultEffortForModel,
|
||||
modelSupportsEffort,
|
||||
modelSupportsMaxEffort,
|
||||
modelSupportsXhighEffort,
|
||||
resolvePickerEffortPersistence,
|
||||
toPersistableEffort,
|
||||
} from '../utils/effort.js';
|
||||
@@ -146,11 +147,19 @@ export function ModelPicker({
|
||||
focusedValue !== NO_PREFERENCE &&
|
||||
marked1MValues.has(focusedValue.replace(/\[1m\]/i, ''));
|
||||
const focusedSupportsEffort = focusedModel ? modelSupportsEffort(focusedModel) : false;
|
||||
const focusedSupportsXhigh = focusedModel ? modelSupportsXhighEffort(focusedModel) : false;
|
||||
const focusedSupportsMax = focusedModel ? modelSupportsMaxEffort(focusedModel) : false;
|
||||
const focusedDefaultEffort = getDefaultEffortLevelForOption(focusedValue);
|
||||
// Clamp display when 'max' is selected but the focused model doesn't support it.
|
||||
// Clamp display when selected effort isn't supported by the focused model.
|
||||
// resolveAppliedEffort() does the same downgrade at API-send time.
|
||||
const displayEffort = effort === 'max' && !focusedSupportsMax ? 'high' : effort;
|
||||
const displayEffort =
|
||||
effort === 'max' && !focusedSupportsMax
|
||||
? focusedSupportsXhigh
|
||||
? 'xhigh'
|
||||
: 'high'
|
||||
: effort === 'xhigh' && !focusedSupportsXhigh
|
||||
? 'high'
|
||||
: effort;
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(value: string) => {
|
||||
@@ -166,10 +175,12 @@ export function ModelPicker({
|
||||
const handleCycleEffort = useCallback(
|
||||
(direction: 'left' | 'right') => {
|
||||
if (!focusedSupportsEffort) return;
|
||||
setEffort(prev => cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsMax));
|
||||
setEffort(prev =>
|
||||
cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsXhigh, focusedSupportsMax),
|
||||
);
|
||||
setHasToggledEffort(true);
|
||||
},
|
||||
[focusedSupportsEffort, focusedSupportsMax, focusedDefaultEffort],
|
||||
[focusedSupportsEffort, focusedSupportsXhigh, focusedSupportsMax, focusedDefaultEffort],
|
||||
);
|
||||
|
||||
useKeybindings(
|
||||
@@ -333,8 +344,19 @@ function EffortLevelIndicator({ effort }: { effort?: EffortLevel }): React.React
|
||||
return <Text color={effort ? 'claude' : 'subtle'}>{effortLevelToSymbol(effort ?? 'low')}</Text>;
|
||||
}
|
||||
|
||||
function cycleEffortLevel(current: EffortLevel, direction: 'left' | 'right', includeMax: boolean): EffortLevel {
|
||||
const levels: EffortLevel[] = includeMax ? ['low', 'medium', 'high', 'max'] : ['low', 'medium', 'high'];
|
||||
function cycleEffortLevel(
|
||||
current: EffortLevel,
|
||||
direction: 'left' | 'right',
|
||||
includeXhigh: boolean,
|
||||
includeMax: boolean,
|
||||
): EffortLevel {
|
||||
const levels: EffortLevel[] = [
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
...(includeXhigh ? (['xhigh'] as const) : []),
|
||||
...(includeMax ? (['max'] as const) : []),
|
||||
];
|
||||
// If the current level isn't in the cycle (e.g. 'max' after switching to a
|
||||
// non-Opus model), clamp to 'high'.
|
||||
const idx = levels.indexOf(current);
|
||||
|
||||
27
src/services/api/openai/__tests__/responsesAdapter.test.ts
Normal file
27
src/services/api/openai/__tests__/responsesAdapter.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { buildResponsesRequest } from '../responsesAdapter.js'
|
||||
|
||||
describe('buildResponsesRequest', () => {
|
||||
test('includes reasoning effort for ChatGPT Responses requests', () => {
|
||||
const request = buildResponsesRequest({
|
||||
model: 'gpt-5.5',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
tools: [],
|
||||
toolChoice: undefined,
|
||||
reasoningEffort: 'xhigh',
|
||||
})
|
||||
|
||||
expect(request.reasoning).toEqual({ effort: 'xhigh' })
|
||||
})
|
||||
|
||||
test('does not include unsupported max_output_tokens parameter', () => {
|
||||
const request = buildResponsesRequest({
|
||||
model: 'gpt-5.5',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
tools: [],
|
||||
toolChoice: undefined,
|
||||
}) as Record<string, unknown>
|
||||
|
||||
expect('max_output_tokens' in request).toBe(false)
|
||||
})
|
||||
})
|
||||
361
src/services/api/openai/chatgptAuth.ts
Normal file
361
src/services/api/openai/chatgptAuth.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { chmod, mkdir, readFile, unlink, writeFile } from 'fs/promises'
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
|
||||
const ISSUER = 'https://auth.openai.com'
|
||||
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
|
||||
const AUTH_FILE = 'openai-chatgpt-auth.json'
|
||||
const REFRESH_SKEW_MS = 5 * 60 * 1000
|
||||
|
||||
export type ChatGPTDeviceCode = {
|
||||
verificationUrl: string
|
||||
userCode: string
|
||||
deviceAuthId: string
|
||||
intervalSeconds: number
|
||||
}
|
||||
|
||||
export type ChatGPTAuthTokens = {
|
||||
idToken: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
accountId?: string
|
||||
lastRefresh?: string
|
||||
}
|
||||
|
||||
export type ChatGPTAuth = {
|
||||
accessToken: string
|
||||
accountId?: string
|
||||
}
|
||||
|
||||
type StoredAuthFile = {
|
||||
auth_mode?: string
|
||||
tokens?: {
|
||||
id_token?: string
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
account_id?: string
|
||||
}
|
||||
last_refresh?: string
|
||||
}
|
||||
|
||||
function authFilePath(): string {
|
||||
return join(getClaudeConfigHomeDirLocal(), AUTH_FILE)
|
||||
}
|
||||
|
||||
function getClaudeConfigHomeDirLocal(): string {
|
||||
return (
|
||||
process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude')
|
||||
).normalize('NFC')
|
||||
}
|
||||
|
||||
function codexAuthFilePath(): string {
|
||||
return join(
|
||||
process.env.CODEX_HOME ?? join(process.env.HOME ?? '', '.codex'),
|
||||
'auth.json',
|
||||
)
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function parseJSONRecord(text: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const value = JSON.parse(text) as unknown
|
||||
return value && typeof value === 'object'
|
||||
? (value as Record<string, unknown>)
|
||||
: null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
const [, payload] = token.split('.')
|
||||
if (!payload) return null
|
||||
try {
|
||||
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = normalized.padEnd(
|
||||
normalized.length + ((4 - (normalized.length % 4)) % 4),
|
||||
'=',
|
||||
)
|
||||
const json = Buffer.from(padded, 'base64').toString('utf8')
|
||||
return parseJSONRecord(json)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getOpenAIAuthClaims(token: string): Record<string, unknown> {
|
||||
const payload = decodeJwtPayload(token)
|
||||
const nested = payload?.['https://api.openai.com/auth']
|
||||
if (nested && typeof nested === 'object') {
|
||||
return nested as Record<string, unknown>
|
||||
}
|
||||
return payload ?? {}
|
||||
}
|
||||
|
||||
function getTokenExpiryMs(token: string): number | null {
|
||||
const payload = decodeJwtPayload(token)
|
||||
const exp = payload?.exp
|
||||
return typeof exp === 'number' ? exp * 1000 : null
|
||||
}
|
||||
|
||||
function extractAccountId(tokens: {
|
||||
idToken?: string
|
||||
accessToken?: string
|
||||
accountId?: string
|
||||
}): string | undefined {
|
||||
if (tokens.accountId) return tokens.accountId
|
||||
for (const token of [tokens.idToken, tokens.accessToken]) {
|
||||
if (!token) continue
|
||||
const claims = getOpenAIAuthClaims(token)
|
||||
const accountId =
|
||||
asString(claims.chatgpt_account_id) ??
|
||||
asString(claims.chatgpt_account_user_id) ??
|
||||
asString(claims.account_id)
|
||||
if (accountId) return accountId
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function readStoredAuth(path: string): Promise<ChatGPTAuthTokens | null> {
|
||||
try {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as StoredAuthFile
|
||||
const tokens = parsed.tokens
|
||||
const idToken = tokens?.id_token
|
||||
const accessToken = tokens?.access_token
|
||||
const refreshToken = tokens?.refresh_token
|
||||
if (!idToken || !accessToken || !refreshToken) return null
|
||||
return {
|
||||
idToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accountId: extractAccountId({
|
||||
idToken,
|
||||
accessToken,
|
||||
accountId: tokens.account_id,
|
||||
}),
|
||||
lastRefresh: parsed.last_refresh,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStoredAuth(tokens: ChatGPTAuthTokens): Promise<void> {
|
||||
const path = authFilePath()
|
||||
await mkdir(getClaudeConfigHomeDirLocal(), { recursive: true })
|
||||
const body: StoredAuthFile = {
|
||||
auth_mode: 'chatgpt',
|
||||
tokens: {
|
||||
id_token: tokens.idToken,
|
||||
access_token: tokens.accessToken,
|
||||
refresh_token: tokens.refreshToken,
|
||||
account_id: extractAccountId(tokens),
|
||||
},
|
||||
last_refresh: new Date().toISOString(),
|
||||
}
|
||||
await writeFile(path, `${JSON.stringify(body, null, 2)}\n`, {
|
||||
mode: 0o600,
|
||||
})
|
||||
await chmod(path, 0o600).catch(() => undefined)
|
||||
}
|
||||
|
||||
async function postJSON<T>(
|
||||
url: string,
|
||||
body: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`ChatGPT auth request failed (${res.status})`)
|
||||
}
|
||||
return (await res.json()) as T
|
||||
}
|
||||
|
||||
async function postForm<T>(url: string, body: URLSearchParams): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(
|
||||
`ChatGPT token request failed (${res.status})${text ? `: ${text}` : ''}`,
|
||||
)
|
||||
}
|
||||
return (await res.json()) as T
|
||||
}
|
||||
|
||||
export async function requestChatGPTDeviceCode(): Promise<ChatGPTDeviceCode> {
|
||||
type UserCodeResponse = {
|
||||
device_auth_id: string
|
||||
user_code?: string
|
||||
usercode?: string
|
||||
interval?: string | number
|
||||
}
|
||||
const data = await postJSON<UserCodeResponse>(
|
||||
`${ISSUER}/api/accounts/deviceauth/usercode`,
|
||||
{ client_id: CLIENT_ID },
|
||||
)
|
||||
const userCode = data.user_code ?? data.usercode
|
||||
if (!data.device_auth_id || !userCode) {
|
||||
throw new Error('ChatGPT auth response did not include a device code')
|
||||
}
|
||||
const interval =
|
||||
typeof data.interval === 'number'
|
||||
? data.interval
|
||||
: Number.parseInt(data.interval ?? '5', 10)
|
||||
return {
|
||||
verificationUrl: `${ISSUER}/codex/device`,
|
||||
userCode,
|
||||
deviceAuthId: data.device_auth_id,
|
||||
intervalSeconds: Number.isFinite(interval) && interval > 0 ? interval : 5,
|
||||
}
|
||||
}
|
||||
|
||||
async function pollForAuthorizationCode(
|
||||
deviceCode: ChatGPTDeviceCode,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ authorizationCode: string; codeVerifier: string }> {
|
||||
type TokenPollResponse = {
|
||||
authorization_code: string
|
||||
code_verifier: string
|
||||
}
|
||||
const started = Date.now()
|
||||
while (Date.now() - started < 15 * 60 * 1000) {
|
||||
if (signal?.aborted) throw new Error('ChatGPT login cancelled')
|
||||
const res = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
device_auth_id: deviceCode.deviceAuthId,
|
||||
user_code: deviceCode.userCode,
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as TokenPollResponse
|
||||
return {
|
||||
authorizationCode: data.authorization_code,
|
||||
codeVerifier: data.code_verifier,
|
||||
}
|
||||
}
|
||||
if (res.status !== 403 && res.status !== 404) {
|
||||
throw new Error(`ChatGPT device auth failed (${res.status})`)
|
||||
}
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, deviceCode.intervalSeconds * 1000),
|
||||
)
|
||||
}
|
||||
throw new Error('ChatGPT device auth timed out after 15 minutes')
|
||||
}
|
||||
|
||||
async function exchangeAuthorizationCode(params: {
|
||||
authorizationCode: string
|
||||
codeVerifier: string
|
||||
}): Promise<ChatGPTAuthTokens> {
|
||||
type TokenResponse = {
|
||||
id_token: string
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
}
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: params.authorizationCode,
|
||||
redirect_uri: `${ISSUER}/deviceauth/callback`,
|
||||
client_id: CLIENT_ID,
|
||||
code_verifier: params.codeVerifier,
|
||||
})
|
||||
const data = await postForm<TokenResponse>(`${ISSUER}/oauth/token`, body)
|
||||
return {
|
||||
idToken: data.id_token,
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accountId: extractAccountId({
|
||||
idToken: data.id_token,
|
||||
accessToken: data.access_token,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTokens(
|
||||
tokens: ChatGPTAuthTokens,
|
||||
): Promise<ChatGPTAuthTokens> {
|
||||
type TokenResponse = {
|
||||
id_token: string
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
}
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: tokens.refreshToken,
|
||||
client_id: CLIENT_ID,
|
||||
scope:
|
||||
'openid profile email offline_access api.connectors.read api.connectors.invoke',
|
||||
})
|
||||
const data = await postForm<TokenResponse>(`${ISSUER}/oauth/token`, body)
|
||||
return {
|
||||
idToken: data.id_token,
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token ?? tokens.refreshToken,
|
||||
accountId: extractAccountId({
|
||||
idToken: data.id_token,
|
||||
accessToken: data.access_token,
|
||||
accountId: tokens.accountId,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeChatGPTDeviceLogin(
|
||||
deviceCode: ChatGPTDeviceCode,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ChatGPTAuthTokens> {
|
||||
const code = await pollForAuthorizationCode(deviceCode, signal)
|
||||
const tokens = await exchangeAuthorizationCode(code)
|
||||
await saveStoredAuth(tokens)
|
||||
return tokens
|
||||
}
|
||||
|
||||
export function isChatGPTAuthEnabled(): boolean {
|
||||
return process.env.OPENAI_AUTH_MODE === 'chatgpt'
|
||||
}
|
||||
|
||||
export async function removeChatGPTAuth(): Promise<void> {
|
||||
await unlink(authFilePath()).catch(error => {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getValidChatGPTAuth(): Promise<ChatGPTAuth> {
|
||||
let tokens = await readStoredAuth(authFilePath())
|
||||
if (!tokens) {
|
||||
tokens = await readStoredAuth(codexAuthFilePath())
|
||||
if (tokens) {
|
||||
logForDebugging('[OpenAI] Using ChatGPT auth from Codex auth.json')
|
||||
}
|
||||
}
|
||||
if (!tokens) {
|
||||
throw new Error(
|
||||
'ChatGPT account is not logged in. Run /login and select ChatGPT account with subscription.',
|
||||
)
|
||||
}
|
||||
const expiresAt = getTokenExpiryMs(tokens.accessToken)
|
||||
if (expiresAt !== null && expiresAt <= Date.now() + REFRESH_SKEW_MS) {
|
||||
tokens = await refreshTokens(tokens)
|
||||
await saveStoredAuth(tokens)
|
||||
}
|
||||
return {
|
||||
accessToken: tokens.accessToken,
|
||||
accountId: tokens.accountId ?? extractAccountId(tokens),
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,13 @@ import {
|
||||
anthropicToolsToOpenAI,
|
||||
anthropicToolChoiceToOpenAI,
|
||||
} from '@ant/model-provider'
|
||||
import { isChatGPTAuthEnabled } from './chatgptAuth.js'
|
||||
import {
|
||||
adaptResponsesStreamToAnthropic,
|
||||
buildResponsesRequest,
|
||||
createChatGPTResponsesStream,
|
||||
type ResponsesReasoningEffort,
|
||||
} from './responsesAdapter.js'
|
||||
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
|
||||
import { toolToAPISchema } from '../../../utils/api.js'
|
||||
import {
|
||||
@@ -62,6 +69,29 @@ import {
|
||||
TOOL_SEARCH_TOOL_NAME,
|
||||
} from '@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
|
||||
function convertToResponsesReasoningEffort(
|
||||
effortValue: unknown,
|
||||
): ResponsesReasoningEffort | undefined {
|
||||
if (effortValue === 'low') return 'low'
|
||||
if (effortValue === 'medium') return 'medium'
|
||||
if (effortValue === 'high') return 'high'
|
||||
if (effortValue === 'xhigh' || effortValue === 'max') return 'xhigh'
|
||||
if (typeof effortValue === 'number') return 'high'
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getChatGPTResponsesReasoningEffort(
|
||||
effortValue: unknown,
|
||||
): ResponsesReasoningEffort | undefined {
|
||||
const envOverride = process.env.CLAUDE_CODE_EFFORT_LEVEL?.toLowerCase()
|
||||
if (envOverride === 'auto' || envOverride === 'unset') return undefined
|
||||
return (
|
||||
convertToResponsesReasoningEffort(envOverride) ??
|
||||
convertToResponsesReasoningEffort(effortValue) ??
|
||||
'medium'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the Anthropic request path's deferred-tool announcement for OpenAI.
|
||||
*
|
||||
@@ -269,6 +299,9 @@ export async function* queryModelOpenAI(
|
||||
)
|
||||
const openaiTools = anthropicToolsToOpenAI(standardTools)
|
||||
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
|
||||
const reasoningEffort = getChatGPTResponsesReasoningEffort(
|
||||
options.effortValue,
|
||||
)
|
||||
|
||||
// 9. Log tool filtering details
|
||||
if (useToolSearch) {
|
||||
@@ -307,32 +340,50 @@ export async function* queryModelOpenAI(
|
||||
options.maxOutputTokensOverride,
|
||||
)
|
||||
|
||||
// 11. Get client
|
||||
const client = getOpenAIClient({
|
||||
maxRetries: 0,
|
||||
fetchOverride: options.fetchOverride as unknown as typeof fetch,
|
||||
source: options.querySource,
|
||||
})
|
||||
|
||||
logForDebugging(
|
||||
`[OpenAI] Calling model=${openaiModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}, thinking=${enableThinking}`,
|
||||
)
|
||||
|
||||
// 12. Call OpenAI API with streaming
|
||||
const requestBody = buildOpenAIRequestBody({
|
||||
model: openaiModel,
|
||||
messages: openaiMessages,
|
||||
tools: openaiTools,
|
||||
toolChoice: openaiToolChoice,
|
||||
enableThinking,
|
||||
maxTokens,
|
||||
temperatureOverride: options.temperatureOverride,
|
||||
})
|
||||
const stream = await client.chat.completions.create(requestBody, { signal })
|
||||
// 11. Call OpenAI API with streaming. ChatGPT subscription auth uses the
|
||||
// Codex Responses backend; API-key/OpenAI-compatible auth keeps the
|
||||
// existing Chat Completions adapter.
|
||||
const adaptedStream = isChatGPTAuthEnabled()
|
||||
? adaptResponsesStreamToAnthropic(
|
||||
await createChatGPTResponsesStream({
|
||||
request: buildResponsesRequest({
|
||||
model: openaiModel,
|
||||
messages: openaiMessages,
|
||||
tools: openaiTools,
|
||||
toolChoice: openaiToolChoice,
|
||||
reasoningEffort,
|
||||
}),
|
||||
signal,
|
||||
fetchOverride: options.fetchOverride as unknown as typeof fetch,
|
||||
}),
|
||||
openaiModel,
|
||||
)
|
||||
: adaptOpenAIStreamToAnthropic(
|
||||
await getOpenAIClient({
|
||||
maxRetries: 0,
|
||||
fetchOverride: options.fetchOverride as unknown as typeof fetch,
|
||||
source: options.querySource,
|
||||
}).chat.completions.create(
|
||||
buildOpenAIRequestBody({
|
||||
model: openaiModel,
|
||||
messages: openaiMessages,
|
||||
tools: openaiTools,
|
||||
toolChoice: openaiToolChoice,
|
||||
enableThinking,
|
||||
maxTokens,
|
||||
temperatureOverride: options.temperatureOverride,
|
||||
}),
|
||||
{ signal },
|
||||
),
|
||||
openaiModel,
|
||||
)
|
||||
|
||||
// 12. Convert OpenAI stream to Anthropic events, then process into
|
||||
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
||||
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, openaiModel)
|
||||
|
||||
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
||||
const contentBlocks: Record<number, any> = {}
|
||||
|
||||
480
src/services/api/openai/responsesAdapter.ts
Normal file
480
src/services/api/openai/responsesAdapter.ts
Normal file
@@ -0,0 +1,480 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import { getValidChatGPTAuth } from './chatgptAuth.js'
|
||||
|
||||
type ResponsesInputItem = Record<string, unknown>
|
||||
type ResponsesTool = Record<string, unknown>
|
||||
export type ResponsesReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh'
|
||||
|
||||
type ResponsesRequest = {
|
||||
model: string
|
||||
stream: true
|
||||
store: false
|
||||
input: ResponsesInputItem[]
|
||||
instructions?: string
|
||||
tools?: ResponsesTool[]
|
||||
tool_choice?: unknown
|
||||
reasoning?: { effort: ResponsesReasoningEffort }
|
||||
parallel_tool_calls?: boolean
|
||||
}
|
||||
|
||||
type AnthropicUsage = {
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens: number
|
||||
cache_read_input_tokens: number
|
||||
}
|
||||
|
||||
function textFromContent(content: unknown): string {
|
||||
if (typeof content === 'string') return content
|
||||
if (!Array.isArray(content)) return ''
|
||||
return content
|
||||
.map(part => {
|
||||
if (!part || typeof part !== 'object') return ''
|
||||
const record = part as Record<string, unknown>
|
||||
if (typeof record.text === 'string') return record.text
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function convertUserContent(content: unknown): unknown {
|
||||
if (typeof content === 'string') return content
|
||||
if (!Array.isArray(content)) return textFromContent(content)
|
||||
const result: Array<Record<string, unknown>> = []
|
||||
for (const part of content) {
|
||||
if (!part || typeof part !== 'object') continue
|
||||
const record = part as Record<string, unknown>
|
||||
if (record.type === 'text' && typeof record.text === 'string') {
|
||||
result.push({ type: 'input_text', text: record.text })
|
||||
} else if (record.type === 'image_url') {
|
||||
const imageUrl = record.image_url as Record<string, unknown> | undefined
|
||||
if (typeof imageUrl?.url === 'string') {
|
||||
result.push({ type: 'input_image', image_url: imageUrl.url })
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.length > 0 ? result : textFromContent(content)
|
||||
}
|
||||
|
||||
function convertMessagesToResponsesInput(messages: unknown[]): {
|
||||
input: ResponsesInputItem[]
|
||||
instructions?: string
|
||||
} {
|
||||
const input: ResponsesInputItem[] = []
|
||||
const instructions: string[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
if (!message || typeof message !== 'object') continue
|
||||
const record = message as Record<string, unknown>
|
||||
const role = record.role
|
||||
|
||||
if (role === 'system' || role === 'developer') {
|
||||
const text = textFromContent(record.content)
|
||||
if (text) instructions.push(text)
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === 'tool') {
|
||||
const callId = record.tool_call_id
|
||||
if (typeof callId === 'string') {
|
||||
input.push({
|
||||
type: 'function_call_output',
|
||||
call_id: callId,
|
||||
output: textFromContent(record.content),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
const text = textFromContent(record.content)
|
||||
if (text) {
|
||||
input.push({ role: 'assistant', content: text })
|
||||
}
|
||||
const toolCalls = record.tool_calls
|
||||
if (Array.isArray(toolCalls)) {
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!toolCall || typeof toolCall !== 'object') continue
|
||||
const tc = toolCall as Record<string, unknown>
|
||||
const fn = tc.function as Record<string, unknown> | undefined
|
||||
const id = typeof tc.id === 'string' ? tc.id : undefined
|
||||
const name = typeof fn?.name === 'string' ? fn.name : undefined
|
||||
if (!id || !name) continue
|
||||
input.push({
|
||||
type: 'function_call',
|
||||
call_id: id,
|
||||
name,
|
||||
arguments: typeof fn?.arguments === 'string' ? fn.arguments : '{}',
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
input.push({
|
||||
role: 'user',
|
||||
content: convertUserContent(record.content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
input,
|
||||
instructions:
|
||||
instructions.length > 0 ? instructions.join('\n\n') : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function convertToolsToResponses(tools: unknown[]): ResponsesTool[] {
|
||||
const result: ResponsesTool[] = []
|
||||
for (const tool of tools) {
|
||||
if (!tool || typeof tool !== 'object') continue
|
||||
const record = tool as Record<string, unknown>
|
||||
const fn = record.function as Record<string, unknown> | undefined
|
||||
const name = typeof fn?.name === 'string' ? fn.name : undefined
|
||||
if (!name) continue
|
||||
result.push({
|
||||
type: 'function',
|
||||
name,
|
||||
description: typeof fn?.description === 'string' ? fn.description : '',
|
||||
parameters:
|
||||
fn?.parameters && typeof fn.parameters === 'object'
|
||||
? fn.parameters
|
||||
: { type: 'object', properties: {} },
|
||||
strict: false,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function convertToolChoiceToResponses(toolChoice: unknown): unknown {
|
||||
if (toolChoice === 'required') return 'required'
|
||||
if (toolChoice === 'auto') return 'auto'
|
||||
if (!toolChoice || typeof toolChoice !== 'object') return toolChoice
|
||||
const record = toolChoice as Record<string, unknown>
|
||||
const fn = record.function as Record<string, unknown> | undefined
|
||||
if (record.type === 'function' && typeof fn?.name === 'string') {
|
||||
return { type: 'function', name: fn.name }
|
||||
}
|
||||
return toolChoice
|
||||
}
|
||||
|
||||
export function buildResponsesRequest(params: {
|
||||
model: string
|
||||
messages: unknown[]
|
||||
tools: unknown[]
|
||||
toolChoice: unknown
|
||||
reasoningEffort?: ResponsesReasoningEffort
|
||||
}): ResponsesRequest {
|
||||
const { input, instructions } = convertMessagesToResponsesInput(
|
||||
params.messages,
|
||||
)
|
||||
const tools = convertToolsToResponses(params.tools)
|
||||
return {
|
||||
model: params.model,
|
||||
stream: true,
|
||||
store: false,
|
||||
input,
|
||||
...(instructions ? { instructions } : {}),
|
||||
...(tools.length > 0 ? { tools } : {}),
|
||||
...(params.toolChoice
|
||||
? { tool_choice: convertToolChoiceToResponses(params.toolChoice) }
|
||||
: {}),
|
||||
...(params.reasoningEffort
|
||||
? { reasoning: { effort: params.reasoningEffort } }
|
||||
: {}),
|
||||
parallel_tool_calls: true,
|
||||
}
|
||||
}
|
||||
|
||||
async function* parseSSE(
|
||||
response: Response,
|
||||
): AsyncGenerator<Record<string, unknown>, void> {
|
||||
if (!response.body) throw new Error('ChatGPT response did not include a body')
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
let splitAt = buffer.indexOf('\n\n')
|
||||
while (splitAt >= 0) {
|
||||
const frame = buffer.slice(0, splitAt)
|
||||
buffer = buffer.slice(splitAt + 2)
|
||||
const data = frame
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.startsWith('data:'))
|
||||
.map(line => line.slice(5).trimStart())
|
||||
.join('\n')
|
||||
if (data && data !== '[DONE]') {
|
||||
const parsed = JSON.parse(data) as unknown
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
yield parsed as Record<string, unknown>
|
||||
}
|
||||
}
|
||||
splitAt = buffer.indexOf('\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractUsage(
|
||||
response: Record<string, unknown> | undefined,
|
||||
): AnthropicUsage {
|
||||
const usage = response?.usage as Record<string, unknown> | undefined
|
||||
const inputDetails = usage?.input_tokens_details as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
return {
|
||||
input_tokens:
|
||||
typeof usage?.input_tokens === 'number' ? usage.input_tokens : 0,
|
||||
output_tokens:
|
||||
typeof usage?.output_tokens === 'number' ? usage.output_tokens : 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens:
|
||||
typeof inputDetails?.cached_tokens === 'number'
|
||||
? inputDetails.cached_tokens
|
||||
: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mapStopReason(response: Record<string, unknown> | undefined): string {
|
||||
if (response?.status === 'incomplete') return 'max_tokens'
|
||||
return 'end_turn'
|
||||
}
|
||||
|
||||
export async function* adaptResponsesStreamToAnthropic(
|
||||
stream: AsyncIterable<Record<string, unknown>>,
|
||||
model: string,
|
||||
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
|
||||
const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
||||
const toolBlocks = new Map<
|
||||
number,
|
||||
{ contentIndex: number; open: boolean; name: string; id: string }
|
||||
>()
|
||||
let started = false
|
||||
let currentContentIndex = -1
|
||||
let textBlockOpen = false
|
||||
let thinkingBlockOpen = false
|
||||
|
||||
const ensureStarted = async function* () {
|
||||
if (started) return
|
||||
started = true
|
||||
yield {
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: messageId,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model,
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
} as unknown as BetaRawMessageStreamEvent
|
||||
}
|
||||
|
||||
for await (const event of stream) {
|
||||
for await (const startedEvent of ensureStarted()) yield startedEvent
|
||||
const type = event.type
|
||||
|
||||
if (type === 'response.output_text.delta') {
|
||||
if (!textBlockOpen) {
|
||||
if (thinkingBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
thinkingBlockOpen = false
|
||||
}
|
||||
currentContentIndex++
|
||||
textBlockOpen = true
|
||||
yield {
|
||||
type: 'content_block_start',
|
||||
index: currentContentIndex,
|
||||
content_block: { type: 'text', text: '' },
|
||||
} as BetaRawMessageStreamEvent
|
||||
}
|
||||
yield {
|
||||
type: 'content_block_delta',
|
||||
index: currentContentIndex,
|
||||
delta: { type: 'text_delta', text: String(event.delta ?? '') },
|
||||
} as BetaRawMessageStreamEvent
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'response.reasoning_text.delta') {
|
||||
if (!thinkingBlockOpen) {
|
||||
if (textBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
textBlockOpen = false
|
||||
}
|
||||
currentContentIndex++
|
||||
thinkingBlockOpen = true
|
||||
yield {
|
||||
type: 'content_block_start',
|
||||
index: currentContentIndex,
|
||||
content_block: { type: 'thinking', thinking: '', signature: '' },
|
||||
} as BetaRawMessageStreamEvent
|
||||
}
|
||||
yield {
|
||||
type: 'content_block_delta',
|
||||
index: currentContentIndex,
|
||||
delta: { type: 'thinking_delta', thinking: String(event.delta ?? '') },
|
||||
} as BetaRawMessageStreamEvent
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'response.output_item.added') {
|
||||
const item = event.item as Record<string, unknown> | undefined
|
||||
const outputIndex =
|
||||
typeof event.output_index === 'number' ? event.output_index : -1
|
||||
if (item?.type === 'function_call' && outputIndex >= 0) {
|
||||
if (textBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
textBlockOpen = false
|
||||
}
|
||||
if (thinkingBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
thinkingBlockOpen = false
|
||||
}
|
||||
currentContentIndex++
|
||||
const id = String(item.call_id ?? item.id ?? `call_${outputIndex}`)
|
||||
const name = String(item.name ?? '')
|
||||
toolBlocks.set(outputIndex, {
|
||||
contentIndex: currentContentIndex,
|
||||
open: true,
|
||||
name,
|
||||
id,
|
||||
})
|
||||
yield {
|
||||
type: 'content_block_start',
|
||||
index: currentContentIndex,
|
||||
content_block: { type: 'tool_use', id, name, input: {} },
|
||||
} as BetaRawMessageStreamEvent
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'response.function_call_arguments.delta') {
|
||||
const outputIndex =
|
||||
typeof event.output_index === 'number' ? event.output_index : -1
|
||||
const block = toolBlocks.get(outputIndex)
|
||||
if (block) {
|
||||
yield {
|
||||
type: 'content_block_delta',
|
||||
index: block.contentIndex,
|
||||
delta: {
|
||||
type: 'input_json_delta',
|
||||
partial_json: String(event.delta ?? ''),
|
||||
},
|
||||
} as BetaRawMessageStreamEvent
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'response.output_item.done') {
|
||||
const outputIndex =
|
||||
typeof event.output_index === 'number' ? event.output_index : -1
|
||||
const block = toolBlocks.get(outputIndex)
|
||||
if (block?.open) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: block.contentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
block.open = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'response.error') {
|
||||
const error = event.error as Record<string, unknown> | undefined
|
||||
throw new Error(String(error?.message ?? 'ChatGPT Responses API error'))
|
||||
}
|
||||
|
||||
if (type === 'response.failed') {
|
||||
const response = event.response as Record<string, unknown> | undefined
|
||||
const error = response?.error as Record<string, unknown> | undefined
|
||||
throw new Error(String(error?.message ?? 'ChatGPT Responses API failed'))
|
||||
}
|
||||
|
||||
if (type === 'response.completed' || type === 'response.incomplete') {
|
||||
if (textBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
textBlockOpen = false
|
||||
}
|
||||
if (thinkingBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: currentContentIndex,
|
||||
} as BetaRawMessageStreamEvent
|
||||
thinkingBlockOpen = false
|
||||
}
|
||||
const response = event.response as Record<string, unknown> | undefined
|
||||
yield {
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: mapStopReason(response), stop_sequence: null },
|
||||
usage: extractUsage(response),
|
||||
} as unknown as BetaRawMessageStreamEvent
|
||||
yield { type: 'message_stop' } as BetaRawMessageStreamEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createChatGPTResponsesStream(params: {
|
||||
request: ResponsesRequest
|
||||
signal: AbortSignal
|
||||
fetchOverride?: typeof fetch
|
||||
}): Promise<AsyncIterable<Record<string, unknown>>> {
|
||||
const auth = await getValidChatGPTAuth()
|
||||
const fetchFn = params.fetchOverride ?? (globalThis.fetch as typeof fetch)
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${auth.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
'OpenAI-Beta': 'responses=experimental',
|
||||
Origin: 'https://chatgpt.com',
|
||||
Referer: 'https://chatgpt.com/',
|
||||
originator: 'claude-code-best',
|
||||
}
|
||||
if (auth.accountId) {
|
||||
headers['ChatGPT-Account-Id'] = auth.accountId
|
||||
}
|
||||
const response = await fetchFn(
|
||||
'https://chatgpt.com/backend-api/codex/responses',
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(params.request),
|
||||
signal: params.signal,
|
||||
},
|
||||
)
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
throw new Error(
|
||||
`ChatGPT Responses API request failed (${response.status})${text ? `: ${text.slice(0, 500)}` : ''}`,
|
||||
)
|
||||
}
|
||||
return parseSSE(response)
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import { isEnvTruthy } from './envUtils.js'
|
||||
import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js'
|
||||
import { resolveAntModel } from './model/antModels.js'
|
||||
import { getAntModelOverrideConfig } from './model/antModels.js'
|
||||
import {
|
||||
isChatGPTAuthMode,
|
||||
isChatGPTCodexReasoningModel,
|
||||
} from './model/chatgptModels.js'
|
||||
|
||||
export type { EffortLevel }
|
||||
|
||||
@@ -32,6 +36,13 @@ export function modelSupportsEffort(model: string): boolean {
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
if (
|
||||
getAPIProvider() === 'openai' &&
|
||||
isChatGPTAuthMode() &&
|
||||
isChatGPTCodexReasoningModel(model)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// Supported by a subset of Claude 4 models
|
||||
if (
|
||||
m.includes('opus-4-7') ||
|
||||
@@ -87,6 +98,13 @@ export function modelSupportsXhighEffort(model: string): boolean {
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
if (
|
||||
getAPIProvider() === 'openai' &&
|
||||
isChatGPTAuthMode() &&
|
||||
isChatGPTCodexReasoningModel(model)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (model.toLowerCase().includes('opus-4-7')) {
|
||||
return true
|
||||
}
|
||||
@@ -200,6 +218,16 @@ export function resolveAppliedEffort(
|
||||
if (resolved === 'xhigh' && !modelSupportsXhighEffort(model)) {
|
||||
return 'high'
|
||||
}
|
||||
// OpenAI Responses uses xhigh as its highest public reasoning effort.
|
||||
// Keep /effort max usable as a familiar alias in ChatGPT subscription mode.
|
||||
if (
|
||||
resolved === 'max' &&
|
||||
getAPIProvider() === 'openai' &&
|
||||
isChatGPTAuthMode() &&
|
||||
modelSupportsXhighEffort(model)
|
||||
) {
|
||||
return 'xhigh'
|
||||
}
|
||||
// API rejects 'max' on non-Opus-4.6 models — downgrade to 'high'.
|
||||
if (resolved === 'max' && !modelSupportsMaxEffort(model)) {
|
||||
return 'high'
|
||||
@@ -347,6 +375,14 @@ export function getDefaultEffortForModel(
|
||||
// the model launch DRI and research. Default effort is a sensitive setting
|
||||
// that can greatly affect model quality and bashing.
|
||||
|
||||
if (
|
||||
getAPIProvider() === 'openai' &&
|
||||
isChatGPTAuthMode() &&
|
||||
isChatGPTCodexReasoningModel(model)
|
||||
) {
|
||||
return 'medium'
|
||||
}
|
||||
|
||||
// Default effort on Opus 4.6 to medium for Pro.
|
||||
// Max/Team also get medium when the tengu_grey_step2 config is enabled.
|
||||
if (
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* config vars (endpoint, project, region, auth) do.
|
||||
*
|
||||
* Note: OpenAI provider uses OPENAI_* env vars (OPENAI_API_KEY, OPENAI_BASE_URL,
|
||||
* OPENAI_MODEL, OPENAI_DEFAULT_*_MODEL, OPENAI_SMALL_FAST_MODEL) which are all
|
||||
* OPENAI_MODEL, OPENAI_AUTH_MODE, OPENAI_DEFAULT_*_MODEL, OPENAI_SMALL_FAST_MODEL) which are all
|
||||
* provider-managed to keep routing config isolated from Anthropic settings.
|
||||
*/
|
||||
const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
@@ -58,6 +58,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
// OpenAI provider specific
|
||||
'OPENAI_AUTH_MODE',
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_BASE_URL',
|
||||
'OPENAI_MODEL',
|
||||
|
||||
54
src/utils/model/chatgptModels.ts
Normal file
54
src/utils/model/chatgptModels.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export type ChatGPTCodexModelOption = {
|
||||
value: string
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const CHATGPT_CODEX_DEFAULT_MODEL = 'gpt-5.5'
|
||||
export const CHATGPT_CODEX_FAST_MODEL = 'gpt-5.4-mini'
|
||||
|
||||
export const CHATGPT_CODEX_MODEL_OPTIONS: ChatGPTCodexModelOption[] = [
|
||||
{
|
||||
value: 'gpt-5.5',
|
||||
label: 'GPT-5.5',
|
||||
description:
|
||||
'Frontier model for complex coding, research, and real-world work',
|
||||
},
|
||||
{
|
||||
value: 'gpt-5.4',
|
||||
label: 'GPT-5.4',
|
||||
description: 'Strong model for everyday coding',
|
||||
},
|
||||
{
|
||||
value: 'gpt-5.4-mini',
|
||||
label: 'GPT-5.4-Mini',
|
||||
description:
|
||||
'Small, fast, and cost-efficient model for simpler coding tasks',
|
||||
},
|
||||
{
|
||||
value: 'gpt-5.3-codex',
|
||||
label: 'GPT-5.3-Codex',
|
||||
description: 'Coding-optimized model',
|
||||
},
|
||||
{
|
||||
value: 'gpt-5.3-codex-spark',
|
||||
label: 'GPT-5.3-Codex-Spark',
|
||||
description: 'Ultra-fast coding model',
|
||||
},
|
||||
{
|
||||
value: 'gpt-5.2',
|
||||
label: 'GPT-5.2',
|
||||
description: 'Optimized for professional work and long-running agents',
|
||||
},
|
||||
]
|
||||
|
||||
export function isChatGPTAuthMode(): boolean {
|
||||
return process.env.OPENAI_AUTH_MODE === 'chatgpt'
|
||||
}
|
||||
|
||||
export function isChatGPTCodexReasoningModel(model: string): boolean {
|
||||
const normalized = model.toLowerCase().replace(/\[1m\]$/, '')
|
||||
return CHATGPT_CODEX_MODEL_OPTIONS.some(
|
||||
option => option.value.toLowerCase() === normalized,
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,11 @@ import { LIGHTNING_BOLT } from '../../constants/figures.js'
|
||||
import { isModelAllowed } from './modelAllowlist.js'
|
||||
import { type ModelAlias, isModelAlias } from './aliases.js'
|
||||
import { capitalize } from '../stringUtils.js'
|
||||
import {
|
||||
CHATGPT_CODEX_DEFAULT_MODEL,
|
||||
CHATGPT_CODEX_FAST_MODEL,
|
||||
isChatGPTAuthMode,
|
||||
} from './chatgptModels.js'
|
||||
|
||||
export type ModelShortName = string
|
||||
export type ModelName = string
|
||||
@@ -36,6 +41,9 @@ export type ModelSetting = ModelName | ModelAlias | null
|
||||
|
||||
export function getSmallFastModel(): ModelName {
|
||||
const provider = getAPIProvider()
|
||||
if (provider === 'openai' && isChatGPTAuthMode()) {
|
||||
return process.env.OPENAI_SMALL_FAST_MODEL ?? CHATGPT_CODEX_FAST_MODEL
|
||||
}
|
||||
// Provider-specific small fast model
|
||||
if (provider === 'openai' && process.env.OPENAI_SMALL_FAST_MODEL) {
|
||||
return process.env.OPENAI_SMALL_FAST_MODEL
|
||||
@@ -115,6 +123,9 @@ export function getBestModel(): ModelName {
|
||||
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
|
||||
export function getDefaultOpusModel(): ModelName {
|
||||
const provider = getAPIProvider()
|
||||
if (provider === 'openai' && isChatGPTAuthMode()) {
|
||||
return CHATGPT_CODEX_DEFAULT_MODEL
|
||||
}
|
||||
// For OpenAI provider, check OPENAI_DEFAULT_OPUS_MODEL first
|
||||
if (provider === 'openai' && process.env.OPENAI_DEFAULT_OPUS_MODEL) {
|
||||
return process.env.OPENAI_DEFAULT_OPUS_MODEL
|
||||
@@ -140,6 +151,9 @@ export function getDefaultOpusModel(): ModelName {
|
||||
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
|
||||
export function getDefaultSonnetModel(): ModelName {
|
||||
const provider = getAPIProvider()
|
||||
if (provider === 'openai' && isChatGPTAuthMode()) {
|
||||
return CHATGPT_CODEX_DEFAULT_MODEL
|
||||
}
|
||||
// For OpenAI provider, check OPENAI_DEFAULT_SONNET_MODEL first
|
||||
if (provider === 'openai' && process.env.OPENAI_DEFAULT_SONNET_MODEL) {
|
||||
return process.env.OPENAI_DEFAULT_SONNET_MODEL
|
||||
@@ -162,6 +176,9 @@ export function getDefaultSonnetModel(): ModelName {
|
||||
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
|
||||
export function getDefaultHaikuModel(): ModelName {
|
||||
const provider = getAPIProvider()
|
||||
if (provider === 'openai' && isChatGPTAuthMode()) {
|
||||
return CHATGPT_CODEX_FAST_MODEL
|
||||
}
|
||||
// For OpenAI provider, check OPENAI_DEFAULT_HAIKU_MODEL first
|
||||
if (provider === 'openai' && process.env.OPENAI_DEFAULT_HAIKU_MODEL) {
|
||||
return process.env.OPENAI_DEFAULT_HAIKU_MODEL
|
||||
|
||||
@@ -33,6 +33,11 @@ import {
|
||||
} from './model.js'
|
||||
import { has1mContext } from '../context.js'
|
||||
import { getGlobalConfig } from '../config.js'
|
||||
import {
|
||||
CHATGPT_CODEX_DEFAULT_MODEL,
|
||||
CHATGPT_CODEX_MODEL_OPTIONS,
|
||||
isChatGPTAuthMode,
|
||||
} from './chatgptModels.js'
|
||||
|
||||
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
|
||||
|
||||
@@ -336,6 +341,23 @@ function getOpusPlanOption(): ModelOption {
|
||||
}
|
||||
}
|
||||
|
||||
function getChatGPTCodexModelOptions(): ModelOption[] {
|
||||
return [
|
||||
{
|
||||
value: null,
|
||||
label: 'Default (recommended)',
|
||||
description: `Use the default ChatGPT Codex model (currently ${CHATGPT_CODEX_DEFAULT_MODEL})`,
|
||||
descriptionForModel: `Default ChatGPT Codex model (currently ${CHATGPT_CODEX_DEFAULT_MODEL})`,
|
||||
},
|
||||
...CHATGPT_CODEX_MODEL_OPTIONS.map(model => ({
|
||||
value: model.value,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
descriptionForModel: `${model.description} (${model.value})`,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
|
||||
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
|
||||
function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||
@@ -357,6 +379,10 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||
]
|
||||
}
|
||||
|
||||
if (getAPIProvider() === 'openai' && isChatGPTAuthMode()) {
|
||||
return getChatGPTCodexModelOptions()
|
||||
}
|
||||
|
||||
if (isClaudeAISubscriber()) {
|
||||
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
|
||||
// Max and Team Premium users: Default = Opus 4.7 1M (merged), plus Opus 4.6 1M
|
||||
|
||||
Reference in New Issue
Block a user