diff --git a/src/costrict/provider/modelMapping.ts b/src/costrict/provider/modelMapping.ts index ac8ecae7c..cdd7c6a13 100644 --- a/src/costrict/provider/modelMapping.ts +++ b/src/costrict/provider/modelMapping.ts @@ -3,7 +3,7 @@ * 将 Anthropic 模型名映射到 CoStrict 模型名 */ -import { getCachedCoStrictModels } from './models.js' +import { getCachedCoStrictModels, getCheapestCoStrictModel } from './models.js' function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null { if (/haiku/i.test(model)) return 'haiku' @@ -34,6 +34,11 @@ export function resolveCoStrictModel(anthropicModel: string): string { const envVar = `COSTRICT_DEFAULT_${family.toUpperCase()}_MODEL` const override = process.env[envVar] if (override) return override + // haiku 族无 env var 时,自动选取 creditConsumption 最低的模型 + if (family === 'haiku') { + const cheapest = getCheapestCoStrictModel() + if (cheapest) return cheapest + } } // 优先级 3: 直接透传原始模型名(用户手动配置的模型名优先于环境变量) diff --git a/src/costrict/provider/models.ts b/src/costrict/provider/models.ts index 37b6c3c5e..b18d3faf7 100644 --- a/src/costrict/provider/models.ts +++ b/src/costrict/provider/models.ts @@ -90,3 +90,17 @@ export function clearModelCache(): void { export function getCachedCoStrictModels(): CoStrictModel[] { return modelCache?.models ?? [] } + +/** + * 从缓存模型列表中选取 creditConsumption 最低的模型, + * 作为 Anthropic Haiku 的等价物用于轻量级辅助请求。 + * 缓存为空时返回 undefined(调用方应 fallback 到主模型)。 + */ +export function getCheapestCoStrictModel(): string | undefined { + const cached = getCachedCoStrictModels() + if (cached.length === 0) return undefined + const sorted = [...cached].sort( + (a, b) => (a.creditConsumption ?? Infinity) - (b.creditConsumption ?? Infinity), + ) + return sorted[0]?.id +} diff --git a/src/memdir/findRelevantMemories.ts b/src/memdir/findRelevantMemories.ts index 843ede3af..78e87e3d3 100644 --- a/src/memdir/findRelevantMemories.ts +++ b/src/memdir/findRelevantMemories.ts @@ -1,7 +1,8 @@ import { feature } from 'bun:bundle' import { logForDebugging } from '../utils/debug.js' import { errorMessage } from '../utils/errors.js' -import { getDefaultSonnetModel } from '../utils/model/model.js' +import { getDefaultSonnetModel, getSmallFastModel } from '../utils/model/model.js' +import { getAPIProvider } from '../utils/model/providers.js' import { sideQuery } from '../utils/sideQuery.js' import type { LangfuseSpan } from '../services/langfuse/index.js' import { jsonParse } from '../utils/slowOperations.js' @@ -99,8 +100,10 @@ async function selectRelevantMemories( : '' try { + const selectionModel = + getAPIProvider() === 'costrict' ? getSmallFastModel() : getDefaultSonnetModel() const result = await sideQuery({ - model: getDefaultSonnetModel(), + model: selectionModel, system: SELECT_MEMORIES_SYSTEM_PROMPT, skipSystemPromptPrefix: true, messages: [ diff --git a/src/services/SessionMemory/sessionMemory.ts b/src/services/SessionMemory/sessionMemory.ts index 2df75aa0d..7a7727475 100644 --- a/src/services/SessionMemory/sessionMemory.ts +++ b/src/services/SessionMemory/sessionMemory.ts @@ -25,6 +25,8 @@ import { runForkedAgent, } from '../../utils/forkedAgent.js' import { getFsImplementation } from '../../utils/fsOperations.js' +import { getSmallFastModel } from '../../utils/model/model.js' +import { getAPIProvider } from '../../utils/model/providers.js' import { type REPLHookContext, registerPostSamplingHook, @@ -322,13 +324,19 @@ const extractSessionMemory = sequential(async function ( // Run session memory extraction using runForkedAgent for prompt caching // runForkedAgent creates an isolated context to prevent mutation of parent state // Pass setupContext.readFileState so the forked agent can edit the memory file + const cacheSafeParams = createCacheSafeParams(context) + const smallModel = getSmallFastModel() + const modelOverride = + getAPIProvider() === 'costrict' + ? { options: { ...cacheSafeParams.toolUseContext.options, mainLoopModel: smallModel, model: smallModel } } + : {} await runForkedAgent({ promptMessages: [createUserMessage({ content: userPrompt })], - cacheSafeParams: createCacheSafeParams(context), + cacheSafeParams, canUseTool: createMemoryFileCanUseTool(memoryPath), querySource: 'session_memory', forkLabel: 'session_memory', - overrides: { readFileState: setupContext.readFileState }, + overrides: { readFileState: setupContext.readFileState, ...modelOverride }, }) // Log extraction event for tracking frequency @@ -424,6 +432,11 @@ export async function manuallyExtractSessionMemory( const systemPrompt = asSystemPrompt(rawSystemPrompt) // Run session memory extraction using runForkedAgent + const smallModelManual = getSmallFastModel() + const modelOverrideManual = + getAPIProvider() === 'costrict' + ? { options: { ...setupContext.options, mainLoopModel: smallModelManual, model: smallModelManual } } + : {} await runForkedAgent({ promptMessages: [createUserMessage({ content: userPrompt })], cacheSafeParams: { @@ -436,7 +449,7 @@ export async function manuallyExtractSessionMemory( canUseTool: createMemoryFileCanUseTool(memoryPath), querySource: 'session_memory', forkLabel: 'session_memory_manual', - overrides: { readFileState: setupContext.readFileState }, + overrides: { readFileState: setupContext.readFileState, ...modelOverrideManual }, }) // Log manual extraction event diff --git a/src/services/extractMemories/extractMemories.ts b/src/services/extractMemories/extractMemories.ts index 26639fc7e..0f9b06f42 100644 --- a/src/services/extractMemories/extractMemories.ts +++ b/src/services/extractMemories/extractMemories.ts @@ -55,6 +55,8 @@ import { import { getFeatureValue_CACHED_MAY_BE_STALE } from '../analytics/growthbook.js' import { logEvent } from '../analytics/index.js' import { sanitizeToolNameForAnalytics } from '../analytics/metadata.js' +import { getSmallFastModel } from '../../utils/model/model.js' +import { getAPIProvider } from '../../utils/model/providers.js' import { buildExtractAutoOnlyPrompt, buildExtractCombinedPrompt, @@ -367,6 +369,11 @@ export function initExtractMemories(): void { const canUseTool = createAutoMemCanUseTool(memoryDir) const cacheSafeParams = createCacheSafeParams(context) + const smallModel = getSmallFastModel() + const modelOverride = + getAPIProvider() === 'costrict' + ? { options: { ...cacheSafeParams.toolUseContext.options, mainLoopModel: smallModel, model: smallModel } } + : {} // Only run extraction every N eligible turns (tengu_bramble_lintel, default 1). // Trailing extractions (from stashed contexts) skip this check since they @@ -415,12 +422,9 @@ export function initExtractMemories(): void { canUseTool, querySource: 'extract_memories', forkLabel: 'extract_memories', - // The extractMemories subagent does not need to record to transcript. - // Doing so can create race conditions with the main thread. skipTranscript: true, - // Well-behaved extractions complete in 2-4 turns (read → write). - // A hard cap prevents verification rabbit-holes from burning turns. maxTurns: 5, + overrides: modelOverride, }) // Advance the cursor only after a successful run. If the agent errors diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index a45328c3e..eae688439 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -36,8 +36,8 @@ export type ModelSetting = ModelName | ModelAlias | null export function getSmallFastModel(): ModelName { const provider = getAPIProvider() - // CoStrict has no haiku alias — use the main loop model to stay on the same provider - if (provider === 'costrict') return getMainLoopModel() + // CoStrict has no haiku alias — resolve to cheapest available model by creditConsumption + if (provider === 'costrict') return getDefaultHaikuModel() // Provider-specific small fast model if (provider === 'openai' && process.env.OPENAI_SMALL_FAST_MODEL) { return process.env.OPENAI_SMALL_FAST_MODEL diff --git a/src/utils/sideQuery.ts b/src/utils/sideQuery.ts index c29888ed8..a96e97f4e 100644 --- a/src/utils/sideQuery.ts +++ b/src/utils/sideQuery.ts @@ -38,6 +38,7 @@ import { getOpenAIClient } from '../services/api/openai/client.js' import { resolveOpenAIModel } from '@ant/model-provider' import { createCoStrictFetch } from '../costrict/provider/fetch.js' import { resolveCoStrictModel } from '../costrict/provider/modelMapping.js' +import { getCachedCoStrictModels } from '../costrict/provider/models.js' import { getCoStrictBaseURL } from '../costrict/provider/auth.js' import { loadCoStrictCredentials } from '../costrict/provider/credentials.js' import { getProxyFetchOptions } from './proxy.js' @@ -157,7 +158,14 @@ export async function sideQuery(opts: SideQueryOptions): Promise { fetch: createCoStrictFetch() as unknown as typeof fetch, defaultHeaders: { 'User-Agent': getUserAgent() }, }) - return sideQueryOpenAICompat(opts, client, resolveCoStrictModel(getMainLoopModel()), 'CoStrict') + const costrictModel = resolveCoStrictModel(opts.model) + const cachedModels = getCachedCoStrictModels() + const isKnown = + cachedModels.length === 0 || cachedModels.some(m => m.id === costrictModel) + const effectiveModel = isKnown + ? costrictModel + : resolveCoStrictModel(getMainLoopModel()) + return sideQueryOpenAICompat(opts, client, effectiveModel, 'CoStrict') } const {