diff --git a/.gitignore b/.gitignore index 6f0a4e069..f2251f2c5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ data .codex/skills/.system/** !.codex/prompts/ !.codex/prompts/** + +.costrict +.claude +.tmp/ \ No newline at end of file diff --git a/src/commands/provider.ts b/src/commands/provider.ts index 19b19c021..e7b0ff101 100644 --- a/src/commands/provider.ts +++ b/src/commands/provider.ts @@ -4,6 +4,7 @@ import { getAPIProvider } from '../utils/model/providers.js' import { updateSettingsForSource } from '../utils/settings/settings.js' import { getSettings_DEPRECATED } from '../utils/settings/settings.js' import { applyConfigEnvironmentVariables } from '../utils/managedEnv.js' +import { getDefaultSonnetModel, getDefaultOpusModel, getDefaultHaikuModel } from '../utils/model/model.js' function getEnvVarForProvider(provider: string): string { switch (provider) { @@ -43,6 +44,28 @@ const call: LocalCommandCall = async (args, context) => { return { type: 'text', value: `Current API provider: ${current}` } } + // Helper function to set default model based on provider + const setDefaultModel = (provider: string) => { + const settings = getSettings_DEPRECATED() || {} + const currentModel = settings.model + + // Only set default if no model has been explicitly set + if (!currentModel) { + let defaultModel: string + switch (provider) { + case 'anthropic': + defaultModel = getDefaultSonnetModel() + break + case 'openai': + case 'gemini': + case 'grok': + default: + defaultModel = getDefaultSonnetModel() + } + updateSettingsForSource('userSettings', { model: defaultModel }) + } + } + // unset - clear settings, fallback to env vars if (arg === 'unset') { updateSettingsForSource('userSettings', { modelType: undefined }) @@ -53,6 +76,7 @@ const call: LocalCommandCall = async (args, context) => { delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_GEMINI delete process.env.CLAUDE_CODE_USE_GROK + delete process.env.CLAUDE_CODE_USE_COSTRICT return { type: 'text', value: 'API provider cleared (will use environment variables).', @@ -83,6 +107,7 @@ const call: LocalCommandCall = async (args, context) => { const hasUrl = !!mergedEnv.OPENAI_BASE_URL if (!hasKey || !hasUrl) { updateSettingsForSource('userSettings', { modelType: 'openai' }) + setDefaultModel('openai') const missing = [] if (!hasKey) missing.push('OPENAI_API_KEY') if (!hasUrl) missing.push('OPENAI_BASE_URL') @@ -99,6 +124,7 @@ const call: LocalCommandCall = async (args, context) => { const hasKey = !!(mergedEnv.GROK_API_KEY || mergedEnv.XAI_API_KEY) if (!hasKey) { updateSettingsForSource('userSettings', { modelType: 'grok' }) + setDefaultModel('grok') return { type: 'text', value: `Switched to Grok provider.\nWarning: Missing env var: GROK_API_KEY (or XAI_API_KEY)\nConfigure it via settings.json env or set manually.`, @@ -113,6 +139,7 @@ const call: LocalCommandCall = async (args, context) => { // GEMINI_BASE_URL is optional (has default) if (!hasKey) { updateSettingsForSource('userSettings', { modelType: 'gemini' }) + setDefaultModel('gemini') return { type: 'text', value: `Switched to Gemini provider.\nWarning: Missing env var: GEMINI_API_KEY\nConfigure it via /login or set manually.`, @@ -131,8 +158,11 @@ const call: LocalCommandCall = async (args, context) => { delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_GEMINI delete process.env.CLAUDE_CODE_USE_GROK + delete process.env.CLAUDE_CODE_USE_COSTRICT // Update settings.json updateSettingsForSource('userSettings', { modelType: arg }) + // Set default model for the provider + setDefaultModel(arg) // Ensure settings.env gets applied to process.env applyConfigEnvironmentVariables() return { type: 'text', value: `API provider set to ${arg}.` } diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 7b973fe75..0f6acd502 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -13,9 +13,11 @@ import { OAuthService } from '../services/oauth/index.js' import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js' import { logError } from '../utils/log.js' import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js' +import { getDefaultSonnetModel } from '../utils/model/model.js' import { Select } from './CustomSelect/select.js' import { Spinner } from './Spinner.js' import TextInput from './TextInput.js' +import { useSetAppState } from '../state/AppState.js' import { fi } from 'zod/v4/locales' type Props = { @@ -28,6 +30,14 @@ type Props = { type OAuthStatus = | { state: 'idle' } // Initial state, waiting to select login method | { state: 'platform_setup' } // Show platform setup info (Bedrock/Vertex/Foundry) + | { + state: 'costrict_waiting' + url: string + } // CoStrict OAuth: browser opened, waiting for user to login + | { + state: 'costrict_model_select' + models: Array<{ id: string; name?: string }> + } // CoStrict: login done, select a model | { state: 'custom_platform' baseUrl: string @@ -94,6 +104,7 @@ export function ConsoleOAuthFlow({ } return { state: 'idle' } }) + const setAppState = useSetAppState() const [pastedCode, setPastedCode] = useState('') const [cursorOffset, setCursorOffset] = useState(0) @@ -273,6 +284,10 @@ export function ConsoleOAuthFlow({ } // Reset modelType to anthropic when using OAuth login updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any) + // Set default model to sonnet for Anthropic provider + updateSettingsForSource('userSettings', { model: getDefaultSonnetModel() } as any) + // Clear costrict env var to prevent conflicts + delete process.env.CLAUDE_CODE_USE_COSTRICT setOAuthStatus({ state: 'success' }) void sendNotification( @@ -438,6 +453,7 @@ function OAuthStatusMessage({ setLoginWithClaudeAi, onDone, }: OAuthStatusMessageProps): React.ReactNode { + const setAppState = useSetAppState() switch (oauthStatus.state) { case 'idle': return ( @@ -453,6 +469,16 @@ function OAuthStatusMessage({ { + process.env.COSTRICT_MODEL = value; + setAppState(prev => ({ ...prev, mainLoopModel: value, mainLoopModelForSession: null })); + setOAuthStatus({ state: 'success' }); + void onDone(); + }} + onCancel={() => { + const selected = sortedModels[0]?.id ?? ''; + process.env.COSTRICT_MODEL = selected; + setAppState(prev => ({ ...prev, mainLoopModel: selected, mainLoopModelForSession: null })); + setOAuthStatus({ state: 'success' }); + void onDone(); + }} + /> + + + ); + } + case 'platform_setup': return ( diff --git a/src/costrict/provider/fetch.ts b/src/costrict/provider/fetch.ts index dabbf532a..14dbefc92 100644 --- a/src/costrict/provider/fetch.ts +++ b/src/costrict/provider/fetch.ts @@ -34,6 +34,10 @@ function getVersion(): string { const VERSION = getVersion() +type CoStrictFetch = typeof fetch & { + preconnect?: (url: string | URL) => void +} + /** * 创建自定义 fetch 函数,用于 CoStrict API 请求 * @@ -43,7 +47,7 @@ const VERSION = getVersion() * 3. 注入 Authorization 和 CoStrict 特有 headers * 4. 反应性 401 错误恢复(自动重试一次) */ -export function createCoStrictFetch() { +export function createCoStrictFetch(): CoStrictFetch { const costrictFetch = async ( input: RequestInfo | URL, init?: RequestInit, @@ -126,20 +130,21 @@ export function createCoStrictFetch() { } // Bun 原生支持 fetch.preconnect(共享连接池预热) // Node.js 没有 preconnect API,降级为 net.createConnection 做 TCP 预热 - if (typeof fetch.preconnect === 'function') { - costrictFetch.preconnect = fetch.preconnect.bind(fetch) - } else { - costrictFetch.preconnect = (url: string | URL) => { - try { - const { hostname, port } = new URL(typeof url === 'string' ? url : url.toString()) - const { createConnection } = require('node:net') as typeof import('node:net') - const sock = createConnection(Number(port) || 443, hostname, () => sock.destroy()) - sock.setTimeout(3000, () => sock.destroy()) - sock.on('error', () => sock.destroy()) - } catch { - // 预连接失败不影响正常流程 - } - } - } - return costrictFetch + const costrictFetchWithPreconnect = Object.assign(costrictFetch, { + preconnect: typeof fetch.preconnect === 'function' + ? fetch.preconnect.bind(fetch) + : (url: string | URL) => { + try { + const { hostname, port } = new URL(typeof url === 'string' ? url : url.toString()) + const { createConnection } = require('node:net') as typeof import('node:net') + const sock = createConnection(Number(port) || 443, hostname, () => sock.destroy()) + sock.setTimeout(3000, () => sock.destroy()) + sock.on('error', () => sock.destroy()) + } catch { + // 预连接失败不影响正常流程 + } + } + }) + + return costrictFetchWithPreconnect as CoStrictFetch } diff --git a/src/costrict/provider/index.ts b/src/costrict/provider/index.ts index b8c72df4a..fc2f916ee 100644 --- a/src/costrict/provider/index.ts +++ b/src/costrict/provider/index.ts @@ -16,12 +16,7 @@ import type { Tools } from '../../Tool.js' import type { Options } from '../../services/api/claude.js' import OpenAI from 'openai' import { getProxyFetchOptions } from '../../utils/proxy.js' -import { anthropicMessagesToOpenAI } from '../../services/api/openai/convertMessages.js' -import { - anthropicToolsToOpenAI, - anthropicToolChoiceToOpenAI, -} from '../../services/api/openai/convertTools.js' -import { adaptOpenAIStreamToAnthropic } from '../../services/api/openai/streamAdapter.js' +import { anthropicMessagesToOpenAI, anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI, adaptOpenAIStreamToAnthropic } from '@ant/model-provider' import { normalizeMessagesForAPI } from '../../utils/messages.js' import { toolToAPISchema } from '../../utils/api.js' import { logForDebugging } from '../../utils/debug.js' diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts index 16798ef84..55a5c2fde 100644 --- a/src/utils/model/modelOptions.ts +++ b/src/utils/model/modelOptions.ts @@ -33,7 +33,6 @@ import { } from './model.js' import { has1mContext } from '../context.js' import { getGlobalConfig } from '../config.js' -import { hasCoStrictCredentialsSync } from '../../costrict/provider/credentials.js' // @[MODEL LAUNCH]: Update all the available and default model option strings below. @@ -380,10 +379,8 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { } // CoStrict provider: 从缓存中动态展示 CoStrict 可用模型 - // 也覆盖其他 provider 但用户已有 CoStrict 凭证的场景(modelType 丢失/竞态) const isCoStrictProvider = getAPIProvider() === 'costrict' - const hasCoStrictCreds = hasCoStrictCredentialsSync() - if (isCoStrictProvider || hasCoStrictCreds) { + if (isCoStrictProvider) { // 延迟 require 避免循环依赖,且该模块只在 costrict provider 下加载 // eslint-disable-next-line @typescript-eslint/no-require-imports const { getCachedCoStrictModels } = @@ -414,21 +411,27 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { ] } - // 用户没有 CoStrict 凭证,显示推荐登录选项 - return [getCoStrictLoginOption()] -} - -/** - * CoStrict 登录推荐选项 - * 当用户未登录 CoStrict 时显示,提示用户可以登录以使用 CoStrict 模型 - */ -function getCoStrictLoginOption(): ModelOption { - return { - value: 'costrict-login', - label: 'CoStrict (Recommended)', - description: - 'Sign in to CoStrict for more models and better pricing · Run /login and select CoStrict', + // PAYG 用户:展示标准模型选项(Sonnet, Opus, Haiku 等) + const paygOptions = [getDefaultOptionForUser(fastMode)] + const customSonnet = getCustomSonnetOption() + if (customSonnet) { + paygOptions.push(customSonnet) + } else { + paygOptions.push(getSonnet46Option()) } + const customOpus = getCustomOpusOption() + if (customOpus) { + paygOptions.push(customOpus) + } else { + paygOptions.push(getOpus46Option(fastMode)) + } + const customHaiku = getCustomHaikuOption() + if (customHaiku) { + paygOptions.push(customHaiku) + } else { + paygOptions.push(getHaikuOption()) + } + return paygOptions } // @[MODEL LAUNCH]: Add the new model ID to the appropriate family pattern below @@ -549,11 +552,6 @@ export function getModelOptions(fastMode = false): ModelOption[] { } else if (initialMainLoopModel !== null) { customModel = initialMainLoopModel } - // 忽略 costrict-login 伪模型值(用户点击了推荐登录选项但未完成登录) - if (customModel === 'costrict-login') { - return filterModelOptionsByAllowlist(options) - } - if (customModel === null || options.some(opt => opt.value === customModel)) { return filterModelOptionsByAllowlist(options) } else if (customModel === 'opusplan') {