fix: 修复 CoStrict provider 模型选择与 token 估算问题
- ConsoleOAuthFlow: 替换 ModelPicker 为直接 Select 组件,避免 getAPIProvider() 竞态导致显示标准 Anthropic 模型列表 - modelOptions: 简化模型选项逻辑,CoStrict 凭证持有者统一走 CoStrict 模型缓存路径,加载中显示提示而非标准模型列表 - provider/index: 回写 usage 和 stop_reason 到已 yield 的 assistant messages,与 Anthropic 原生路径保持一致 - tokenEstimation: 非 Anthropic 原生 provider(costrict/openai/ grok/gemini)统一使用粗略 token 估算 - 品牌文案: Claude → CoStrict 相关措辞修正
This commit is contained in:
parent
531bd9f735
commit
fbeaf1395e
|
|
@ -17,7 +17,6 @@ import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settin
|
||||||
import { Select } from './CustomSelect/select.js';
|
import { Select } from './CustomSelect/select.js';
|
||||||
import { Spinner } from './Spinner.js';
|
import { Spinner } from './Spinner.js';
|
||||||
import TextInput from './TextInput.js';
|
import TextInput from './TextInput.js';
|
||||||
import { ModelPicker } from './ModelPicker.js';
|
|
||||||
import { useSetAppState } from '../state/AppState.js';
|
import { useSetAppState } from '../state/AppState.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -1260,25 +1259,43 @@ function OAuthStatusMessage({
|
||||||
|
|
||||||
case 'costrict_model_select': {
|
case 'costrict_model_select': {
|
||||||
const sortedModels = [...oauthStatus.models].sort((a, b) => a.id.localeCompare(b.id));
|
const sortedModels = [...oauthStatus.models].sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
// 直接使用已获取的模型列表渲染选项,避免通用 ModelPicker 因 getAPIProvider() 竞态
|
||||||
|
// 导致显示标准 Anthropic 模型列表而非 CoStrict 动态模型
|
||||||
|
const costrictOptions = sortedModels.map(m => ({
|
||||||
|
value: m.id,
|
||||||
|
label: m.name ?? m.id,
|
||||||
|
description:
|
||||||
|
m.id === 'Auto'
|
||||||
|
? `${Math.round(((m as any).creditDiscount ?? 0) * 100)}% discount`
|
||||||
|
: `${(m as any).creditConsumption ?? '?'}x credit`,
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<ModelPicker
|
<Box flexDirection="column">
|
||||||
initial={sortedModels[0]?.id ?? null}
|
<Box marginBottom={1} flexDirection="column">
|
||||||
headerText="Login successful. Select a CoStrict model to use:"
|
<Text color="remember" bold>
|
||||||
onSelect={model => {
|
Select model
|
||||||
const selected = model ?? sortedModels[0]?.id ?? '';
|
</Text>
|
||||||
process.env.COSTRICT_MODEL = selected;
|
<Text dimColor>Login successful. Select a CoStrict model to use:</Text>
|
||||||
setAppState(prev => ({ ...prev, mainLoopModel: selected, mainLoopModelForSession: null }));
|
</Box>
|
||||||
setOAuthStatus({ state: 'success' });
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
void onDone();
|
<Select
|
||||||
}}
|
options={costrictOptions}
|
||||||
onCancel={() => {
|
onChange={(value: string) => {
|
||||||
const selected = sortedModels[0]?.id ?? '';
|
process.env.COSTRICT_MODEL = value;
|
||||||
process.env.COSTRICT_MODEL = selected;
|
setAppState(prev => ({ ...prev, mainLoopModel: value, mainLoopModelForSession: null }));
|
||||||
setAppState(prev => ({ ...prev, mainLoopModel: selected, mainLoopModelForSession: null }));
|
setOAuthStatus({ state: 'success' });
|
||||||
setOAuthStatus({ state: 'success' });
|
void onDone();
|
||||||
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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ export function ModelPicker({
|
||||||
</Text>
|
</Text>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
{headerText ??
|
{headerText ??
|
||||||
'Switch between Claude models. Applies to this session and future CoStrict sessions. For other/previous model names, specify with --model.'}
|
'Switch between CoStrict models. Applies to this session and future CoStrict sessions. For other/previous model names, specify with --model.'}
|
||||||
</Text>
|
</Text>
|
||||||
{sessionModel && (
|
{sessionModel && (
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
|
|
|
||||||
|
|
@ -694,7 +694,7 @@ export async function computeSimpleEnvInfo(
|
||||||
knowledgeCutoffMessage,
|
knowledgeCutoffMessage,
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
process.env.USER_TYPE === 'ant' && isUndercover()
|
||||||
? null
|
? null
|
||||||
: `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`,
|
: `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable CoStrict models.`,
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
process.env.USER_TYPE === 'ant' && isUndercover()
|
||||||
? null
|
? null
|
||||||
: `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (costrict.ai), and IDE extensions (VS Code, JetBrains).`,
|
: `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (costrict.ai), and IDE extensions (VS Code, JetBrains).`,
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,10 @@ export async function* queryModelCoStrict(
|
||||||
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel)
|
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel)
|
||||||
|
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, any> = {}
|
||||||
|
// 跟踪已 yield 的 assistant messages,用于 message_delta 时回写 usage
|
||||||
|
const yieldedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: any
|
||||||
|
let stopReason: string | null = null
|
||||||
let usage = {
|
let usage = {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
output_tokens: 0,
|
output_tokens: 0,
|
||||||
|
|
@ -194,18 +197,33 @@ export async function* queryModelCoStrict(
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
content: normalizeContentFromAPI([block], tools, options.agentId),
|
||||||
|
usage,
|
||||||
},
|
},
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
yieldedMessages.push(m)
|
||||||
yield m
|
yield m
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = (event as any).usage
|
||||||
if (deltaUsage) usage = { ...usage, ...deltaUsage }
|
if (deltaUsage) usage = { ...usage, ...deltaUsage }
|
||||||
|
// 回写 usage 到已 yield 的 assistant messages
|
||||||
|
// 与 Anthropic 原生路径 claude.ts:2298 保持一致
|
||||||
|
for (const msg of yieldedMessages) {
|
||||||
|
msg.message.usage = usage
|
||||||
|
}
|
||||||
|
// 记录 stop_reason,回写到最后的 message
|
||||||
|
if ((event as any).delta?.stop_reason != null) {
|
||||||
|
stopReason = (event as any).delta.stop_reason
|
||||||
|
const lastMsg = yieldedMessages[yieldedMessages.length - 1]
|
||||||
|
if (lastMsg) {
|
||||||
|
lastMsg.message.stop_reason = stopReason
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_stop':
|
case 'message_stop':
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,13 @@ export async function countMessagesTokensWithAPI(
|
||||||
return withTokenCountVCR(messages, tools, async () => {
|
return withTokenCountVCR(messages, tools, async () => {
|
||||||
try {
|
try {
|
||||||
const provider = getAPIProvider()
|
const provider = getAPIProvider()
|
||||||
if (provider === 'gemini') {
|
// 非 Anthropic 原生 provider 不支持 countTokens API,直接使用粗略估算
|
||||||
|
if (
|
||||||
|
provider === 'gemini' ||
|
||||||
|
provider === 'openai' ||
|
||||||
|
provider === 'grok' ||
|
||||||
|
provider === 'costrict'
|
||||||
|
) {
|
||||||
return roughTokenCountEstimationForAPIRequest(messages, tools)
|
return roughTokenCountEstimationForAPIRequest(messages, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,7 +264,13 @@ export async function countTokensViaHaikuFallback(
|
||||||
tools: Anthropic.Beta.Messages.BetaToolUnion[],
|
tools: Anthropic.Beta.Messages.BetaToolUnion[],
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const provider = getAPIProvider()
|
const provider = getAPIProvider()
|
||||||
if (provider === 'gemini') {
|
// 非 Anthropic 原生 provider 不支持 countTokens API,直接使用粗略估算
|
||||||
|
if (
|
||||||
|
provider === 'gemini' ||
|
||||||
|
provider === 'openai' ||
|
||||||
|
provider === 'grok' ||
|
||||||
|
provider === 'costrict'
|
||||||
|
) {
|
||||||
return roughTokenCountEstimationForAPIRequest(messages, tools)
|
return roughTokenCountEstimationForAPIRequest(messages, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ export function modelSupportsAutoMode(model: string): boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
if (process.env.USER_TYPE === 'ant') {
|
||||||
// Denylist: block known-unsupported claude models, allow everything else (ant-internal models etc.)
|
// Denylist: block known-unsupported CoStrict models, allow everything else (ant-internal models etc.)
|
||||||
if (m.includes('claude-3-')) return false
|
if (m.includes('claude-3-')) return false
|
||||||
// claude-*-4 not followed by -[6-9]: blocks bare -4, -4-YYYYMMDD, -4@, -4-0 thru -4-5
|
// claude-*-4 not followed by -[6-9]: blocks bare -4, -4-YYYYMMDD, -4@, -4-0 thru -4-5
|
||||||
if (/claude-(opus|sonnet|haiku)-4(?!-[6-9])/.test(m)) return false
|
if (/claude-(opus|sonnet|haiku)-4(?!-[6-9])/.test(m)) return false
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,10 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoStrict provider: 从缓存中动态展示 CoStrict 可用模型
|
// CoStrict provider: 从缓存中动态展示 CoStrict 可用模型
|
||||||
if (getAPIProvider() === 'costrict') {
|
// 也覆盖其他 provider 但用户已有 CoStrict 凭证的场景(modelType 丢失/竞态)
|
||||||
|
const isCoStrictProvider = getAPIProvider() === 'costrict'
|
||||||
|
const hasCoStrictCreds = hasCoStrictCredentialsSync()
|
||||||
|
if (isCoStrictProvider || hasCoStrictCreds) {
|
||||||
// 延迟 require 避免循环依赖,且该模块只在 costrict provider 下加载
|
// 延迟 require 避免循环依赖,且该模块只在 costrict provider 下加载
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const { getCachedCoStrictModels } =
|
const { getCachedCoStrictModels } =
|
||||||
|
|
@ -400,82 +403,19 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 缓存尚未加载(凭证过期或首次加载中),提示用户重新登录
|
// 缓存尚未加载(启动时预取未完成),显示提示而非标准模型列表
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: null,
|
value: null,
|
||||||
label: 'CoStrict (not logged in)',
|
label: 'CoStrict (loading models...)',
|
||||||
description: 'Run /login to sign in with CoStrict again',
|
description:
|
||||||
|
'CoStrict models are being loaded, please try again in a moment',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku
|
// 用户没有 CoStrict 凭证,显示推荐登录选项
|
||||||
if (getAPIProvider() === 'firstParty') {
|
return [getCoStrictLoginOption()]
|
||||||
// 当用户未登录 CoStrict 时,只显示推荐登录选项
|
|
||||||
if (!hasCoStrictCredentialsSync()) {
|
|
||||||
return [getCoStrictLoginOption()]
|
|
||||||
}
|
|
||||||
|
|
||||||
const payg1POptions: ModelOption[] = []
|
|
||||||
payg1POptions.push(getDefaultOptionForUser(fastMode))
|
|
||||||
|
|
||||||
if (checkSonnet1mAccess()) {
|
|
||||||
payg1POptions.push(getSonnet46_1MOption())
|
|
||||||
}
|
|
||||||
if (isOpus1mMergeEnabled()) {
|
|
||||||
payg1POptions.push(getMergedOpus1MOption(fastMode))
|
|
||||||
} else {
|
|
||||||
payg1POptions.push(getOpus46Option(fastMode))
|
|
||||||
if (checkOpus1mAccess()) {
|
|
||||||
payg1POptions.push(getOpus46_1MOption(fastMode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
payg1POptions.push(getHaiku45Option())
|
|
||||||
|
|
||||||
return payg1POptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.1/Opus 4.6/Opus1M + Haiku + Opus 4.1
|
|
||||||
|
|
||||||
// 当用户未登录 CoStrict 时,只显示推荐登录选项
|
|
||||||
if (getAPIProvider() !== 'costrict' && !hasCoStrictCredentialsSync()) {
|
|
||||||
return [getCoStrictLoginOption()]
|
|
||||||
}
|
|
||||||
|
|
||||||
const payg3pOptions: ModelOption[] = []
|
|
||||||
payg3pOptions.push(getDefaultOptionForUser(fastMode))
|
|
||||||
|
|
||||||
const customSonnet = getCustomSonnetOption()
|
|
||||||
if (customSonnet !== undefined) {
|
|
||||||
payg3pOptions.push(customSonnet)
|
|
||||||
} else {
|
|
||||||
// Add Sonnet 4.6 since Sonnet 4.5 is the default
|
|
||||||
payg3pOptions.push(getSonnet46Option())
|
|
||||||
if (checkSonnet1mAccess()) {
|
|
||||||
payg3pOptions.push(getSonnet46_1MOption())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const customOpus = getCustomOpusOption()
|
|
||||||
if (customOpus !== undefined) {
|
|
||||||
payg3pOptions.push(customOpus)
|
|
||||||
} else {
|
|
||||||
// Add Opus 4.1, Opus 4.6 and Opus 4.6 1M
|
|
||||||
payg3pOptions.push(getOpus41Option()) // This is the default opus
|
|
||||||
payg3pOptions.push(getOpus46Option(fastMode))
|
|
||||||
if (checkOpus1mAccess()) {
|
|
||||||
payg3pOptions.push(getOpus46_1MOption(fastMode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const customHaiku = getCustomHaikuOption()
|
|
||||||
if (customHaiku !== undefined) {
|
|
||||||
payg3pOptions.push(customHaiku)
|
|
||||||
} else {
|
|
||||||
payg3pOptions.push(getHaikuOption())
|
|
||||||
}
|
|
||||||
|
|
||||||
return payg3pOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user