mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
fix: use provider context windows for transcript estimates
Desktop chat can fall back to persisted transcript estimates when live inspection context is unavailable. That path previously only used built-in model windows, so custom provider settings such as MiniMax-M3 with a 1,000,000-token window still displayed the default 200,000-token estimate. Resolve transcript context windows from the session or active provider runtime env first, then fall back to built-ins. Constraint: Provider model context windows are stored in provider runtime env, not always in the server process env. Rejected: Change the built-in MiniMax-M3 window to 1,000,000 | upstream preset correctly keeps MiniMax-M3 at 204,800 by default, while user overrides must remain provider-specific. Confidence: high Scope-risk: moderate Directive: Keep transcript estimates aligned with session/provider runtime env before consulting built-in model defaults. Tested: git diff --check Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "active provider model context windows|Sonnet 4.6 transcript usage|low-trust media" Tested: bun test src/server/__tests__/provider-presets.test.ts src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/utils/__tests__/context.test.ts src/utils/__tests__/contextBudget.test.ts Tested: bun test src/server/__tests__/conversations.test.ts src/server/__tests__/conversation-service.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/provider-presets.test.ts src/server/__tests__/provider-runtime-env.test.ts src/utils/__tests__/context.test.ts src/utils/__tests__/contextBudget.test.ts Not-tested: bun run check:server blocked before execution by expired quarantine entries: server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
This commit is contained in:
parent
8716faf2c1
commit
247da5072f
@ -530,6 +530,84 @@ describe('ConversationService', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should use active provider model context windows for transcript estimates', async () => {
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const previousNodeEnv = process.env.NODE_ENV
|
||||
const previousModelContextWindows = process.env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS
|
||||
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-transcript-provider-'))
|
||||
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-workdir-provider-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpConfigDir
|
||||
process.env.NODE_ENV = 'development'
|
||||
delete process.env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS
|
||||
|
||||
try {
|
||||
const providerService = new ProviderService()
|
||||
const provider = await providerService.addProvider({
|
||||
presetId: 'minimax',
|
||||
name: 'MiniMax',
|
||||
apiKey: 'provider-key',
|
||||
authStrategy: 'auth_token',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiFormat: 'anthropic',
|
||||
models: {
|
||||
main: 'MiniMax-M3',
|
||||
haiku: 'MiniMax-M3',
|
||||
sonnet: 'MiniMax-M3',
|
||||
opus: 'MiniMax-M3',
|
||||
},
|
||||
modelContextWindows: {
|
||||
'MiniMax-M3': 1_000_000,
|
||||
},
|
||||
})
|
||||
await providerService.activateProvider(provider.id)
|
||||
|
||||
const svc = new SessionService()
|
||||
const { sessionId } = await svc.createSession(workDir)
|
||||
const found = await svc.findSessionFile(sessionId)
|
||||
expect(found).not.toBeNull()
|
||||
|
||||
await fs.appendFile(found!.filePath, JSON.stringify({
|
||||
type: 'assistant',
|
||||
uuid: crypto.randomUUID(),
|
||||
timestamp: '2026-04-27T12:00:00.000Z',
|
||||
cwd: workDir,
|
||||
version: '999.0.0-test',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: 'MiniMax-M3',
|
||||
content: [{ type: 'text', text: 'hello' }],
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 20,
|
||||
},
|
||||
},
|
||||
}) + '\n')
|
||||
|
||||
const contextEstimate = await svc.getTranscriptContextEstimate(sessionId)
|
||||
|
||||
expect(contextEstimate?.model).toBe('MiniMax-M3')
|
||||
expect(contextEstimate?.rawMaxTokens).toBe(1_000_000)
|
||||
} finally {
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
||||
}
|
||||
if (previousNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV
|
||||
} else {
|
||||
process.env.NODE_ENV = previousNodeEnv
|
||||
}
|
||||
if (previousModelContextWindows === undefined) {
|
||||
delete process.env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS = previousModelContextWindows
|
||||
}
|
||||
await fs.rm(tmpConfigDir, { recursive: true, force: true })
|
||||
await fs.rm(workDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('should not report transcript context as full for low-trust media usage spikes', async () => {
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const previousNodeEnv = process.env.NODE_ENV
|
||||
|
||||
@ -19,7 +19,12 @@ import {
|
||||
MODEL_CONTEXT_WINDOW_DEFAULT,
|
||||
getContextWindowForModel,
|
||||
getModelMaxOutputTokens,
|
||||
is1mContextDisabled,
|
||||
} from '../../utils/context.js'
|
||||
import {
|
||||
MODEL_CONTEXT_WINDOWS_ENV_KEY,
|
||||
getModelContextWindowFromEnvValue,
|
||||
} from '../../utils/model/modelContextWindows.js'
|
||||
import {
|
||||
calculateContextBudget,
|
||||
getProviderUsageTrust,
|
||||
@ -36,6 +41,7 @@ import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
||||
import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
|
||||
import { cleanSessionTitleSource } from '../../utils/sessionTitleText.js'
|
||||
import { roughTokenCountEstimationForMessages } from '../../services/tokenEstimation.js'
|
||||
import { ProviderService } from './providerService.js'
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@ -269,6 +275,8 @@ const TASK_NOTIFICATION_BLOCK_RE = /<task-notification>\s*[\s\S]*?<\/task-notifi
|
||||
// ============================================================================
|
||||
|
||||
export class SessionService {
|
||||
private providerService = new ProviderService()
|
||||
|
||||
private readonly sessionListCacheTtlMs = 5_000
|
||||
private readonly sessionListCache = new Map<string, {
|
||||
expiresAt: number
|
||||
@ -1288,7 +1296,43 @@ export class SessionService {
|
||||
return `$${cost > 0.5 ? (Math.round(cost * 100) / 100).toFixed(2) : cost.toFixed(4)}`
|
||||
}
|
||||
|
||||
private getTranscriptContextWindow(model: string): number {
|
||||
private async getProviderContextWindowForSession(
|
||||
sessionId: string,
|
||||
model: string,
|
||||
): Promise<number | undefined> {
|
||||
const launchInfo = await this.getSessionLaunchInfo(sessionId).catch(() => null)
|
||||
const providerIds: string[] = []
|
||||
|
||||
if (typeof launchInfo?.runtimeProviderId === 'string') {
|
||||
providerIds.push(launchInfo.runtimeProviderId)
|
||||
} else if (launchInfo?.runtimeProviderId !== null) {
|
||||
const { activeId } = await this.providerService.listProviders().catch(() => ({ activeId: null }))
|
||||
if (activeId) providerIds.push(activeId)
|
||||
}
|
||||
|
||||
for (const providerId of providerIds) {
|
||||
const env = await this.providerService.getProviderRuntimeEnv(providerId).catch(() => null)
|
||||
const contextWindow = getModelContextWindowFromEnvValue(
|
||||
model,
|
||||
env?.[MODEL_CONTEXT_WINDOWS_ENV_KEY],
|
||||
)
|
||||
if (contextWindow !== undefined) {
|
||||
if (contextWindow > MODEL_CONTEXT_WINDOW_DEFAULT && is1mContextDisabled()) {
|
||||
return MODEL_CONTEXT_WINDOW_DEFAULT
|
||||
}
|
||||
return contextWindow
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async getTranscriptContextWindow(sessionId: string, model: string): Promise<number> {
|
||||
const providerContextWindow = await this.getProviderContextWindowForSession(sessionId, model)
|
||||
if (providerContextWindow !== undefined) {
|
||||
return providerContextWindow
|
||||
}
|
||||
|
||||
try {
|
||||
return getContextWindowForModel(model)
|
||||
} catch (err) {
|
||||
@ -1362,7 +1406,7 @@ export class SessionService {
|
||||
|
||||
if (!latest) return null
|
||||
|
||||
const rawMaxTokens = this.getTranscriptContextWindow(latest.model)
|
||||
const rawMaxTokens = await this.getTranscriptContextWindow(sessionId, latest.model)
|
||||
const promptTokens = latest.inputTokens + latest.cacheReadInputTokens + latest.cacheCreationInputTokens
|
||||
const transcriptMessages = entries.filter(entry =>
|
||||
entry.type === 'user' || entry.type === 'assistant' || entry.type === 'attachment',
|
||||
@ -1502,7 +1546,7 @@ export class SessionService {
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
costDisplay: '$0.0000',
|
||||
contextWindow: this.getTranscriptContextWindow(model),
|
||||
contextWindow: await this.getTranscriptContextWindow(sessionId, model),
|
||||
maxOutputTokens: getModelMaxOutputTokens(model).default,
|
||||
}
|
||||
models.set(model, modelUsage)
|
||||
|
||||
@ -61,33 +61,25 @@ function normalizeWindow(value: unknown): number | undefined {
|
||||
return value
|
||||
}
|
||||
|
||||
function parseConfiguredContextWindows(): Record<string, number> {
|
||||
const raw = process.env[MODEL_CONTEXT_WINDOWS_ENV_KEY]
|
||||
if (!raw?.trim()) {
|
||||
function normalizeConfiguredContextWindows(parsed: unknown): Record<string, number> {
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return {}
|
||||
const windows: Record<string, number> = {}
|
||||
for (const [model, value] of Object.entries(parsed)) {
|
||||
const normalized = normalizeWindow(value)
|
||||
if (normalized !== undefined) {
|
||||
windows[normalizeModelContextKey(model)] = normalized
|
||||
}
|
||||
|
||||
const windows: Record<string, number> = {}
|
||||
for (const [model, value] of Object.entries(parsed)) {
|
||||
const normalized = normalizeWindow(value)
|
||||
if (normalized !== undefined) {
|
||||
windows[normalizeModelContextKey(model)] = normalized
|
||||
}
|
||||
}
|
||||
return windows
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
return windows
|
||||
}
|
||||
|
||||
function getConfiguredModelContextWindow(model: string): number | undefined {
|
||||
const configured = parseConfiguredContextWindows()
|
||||
function findConfiguredModelContextWindow(
|
||||
model: string,
|
||||
configured: Record<string, number>,
|
||||
): number | undefined {
|
||||
const normalizedModel = normalizeModelContextKey(model)
|
||||
const exact = configured[normalizedModel]
|
||||
if (exact !== undefined) {
|
||||
@ -105,6 +97,42 @@ function getConfiguredModelContextWindow(model: string): number | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getModelContextWindowFromEnvValue(
|
||||
model: string,
|
||||
raw: string | undefined,
|
||||
): number | undefined {
|
||||
if (!raw?.trim()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
return findConfiguredModelContextWindow(
|
||||
model,
|
||||
normalizeConfiguredContextWindows(parsed),
|
||||
)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfiguredContextWindows(): Record<string, number> {
|
||||
const raw = process.env[MODEL_CONTEXT_WINDOWS_ENV_KEY]
|
||||
if (!raw?.trim()) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeConfiguredContextWindows(JSON.parse(raw) as Record<string, unknown>)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function getConfiguredModelContextWindow(model: string): number | undefined {
|
||||
return findConfiguredModelContextWindow(model, parseConfiguredContextWindows())
|
||||
}
|
||||
|
||||
function getBuiltInModelContextWindow(model: string): number | undefined {
|
||||
const normalizedModel = normalizeModelContextKey(model)
|
||||
const exact = DIRECT_MODEL_CONTEXT_WINDOWS[normalizedModel]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user