mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +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 () => {
|
it('should not report transcript context as full for low-trust media usage spikes', async () => {
|
||||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
const previousNodeEnv = process.env.NODE_ENV
|
const previousNodeEnv = process.env.NODE_ENV
|
||||||
|
|||||||
@ -19,7 +19,12 @@ import {
|
|||||||
MODEL_CONTEXT_WINDOW_DEFAULT,
|
MODEL_CONTEXT_WINDOW_DEFAULT,
|
||||||
getContextWindowForModel,
|
getContextWindowForModel,
|
||||||
getModelMaxOutputTokens,
|
getModelMaxOutputTokens,
|
||||||
|
is1mContextDisabled,
|
||||||
} from '../../utils/context.js'
|
} from '../../utils/context.js'
|
||||||
|
import {
|
||||||
|
MODEL_CONTEXT_WINDOWS_ENV_KEY,
|
||||||
|
getModelContextWindowFromEnvValue,
|
||||||
|
} from '../../utils/model/modelContextWindows.js'
|
||||||
import {
|
import {
|
||||||
calculateContextBudget,
|
calculateContextBudget,
|
||||||
getProviderUsageTrust,
|
getProviderUsageTrust,
|
||||||
@ -36,6 +41,7 @@ import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
|||||||
import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
|
import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
|
||||||
import { cleanSessionTitleSource } from '../../utils/sessionTitleText.js'
|
import { cleanSessionTitleSource } from '../../utils/sessionTitleText.js'
|
||||||
import { roughTokenCountEstimationForMessages } from '../../services/tokenEstimation.js'
|
import { roughTokenCountEstimationForMessages } from '../../services/tokenEstimation.js'
|
||||||
|
import { ProviderService } from './providerService.js'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -269,6 +275,8 @@ const TASK_NOTIFICATION_BLOCK_RE = /<task-notification>\s*[\s\S]*?<\/task-notifi
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export class SessionService {
|
export class SessionService {
|
||||||
|
private providerService = new ProviderService()
|
||||||
|
|
||||||
private readonly sessionListCacheTtlMs = 5_000
|
private readonly sessionListCacheTtlMs = 5_000
|
||||||
private readonly sessionListCache = new Map<string, {
|
private readonly sessionListCache = new Map<string, {
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
@ -1288,7 +1296,43 @@ export class SessionService {
|
|||||||
return `$${cost > 0.5 ? (Math.round(cost * 100) / 100).toFixed(2) : cost.toFixed(4)}`
|
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 {
|
try {
|
||||||
return getContextWindowForModel(model)
|
return getContextWindowForModel(model)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1362,7 +1406,7 @@ export class SessionService {
|
|||||||
|
|
||||||
if (!latest) return null
|
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 promptTokens = latest.inputTokens + latest.cacheReadInputTokens + latest.cacheCreationInputTokens
|
||||||
const transcriptMessages = entries.filter(entry =>
|
const transcriptMessages = entries.filter(entry =>
|
||||||
entry.type === 'user' || entry.type === 'assistant' || entry.type === 'attachment',
|
entry.type === 'user' || entry.type === 'assistant' || entry.type === 'attachment',
|
||||||
@ -1502,7 +1546,7 @@ export class SessionService {
|
|||||||
webSearchRequests: 0,
|
webSearchRequests: 0,
|
||||||
costUSD: 0,
|
costUSD: 0,
|
||||||
costDisplay: '$0.0000',
|
costDisplay: '$0.0000',
|
||||||
contextWindow: this.getTranscriptContextWindow(model),
|
contextWindow: await this.getTranscriptContextWindow(sessionId, model),
|
||||||
maxOutputTokens: getModelMaxOutputTokens(model).default,
|
maxOutputTokens: getModelMaxOutputTokens(model).default,
|
||||||
}
|
}
|
||||||
models.set(model, modelUsage)
|
models.set(model, modelUsage)
|
||||||
|
|||||||
@ -61,33 +61,25 @@ function normalizeWindow(value: unknown): number | undefined {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseConfiguredContextWindows(): Record<string, number> {
|
function normalizeConfiguredContextWindows(parsed: unknown): Record<string, number> {
|
||||||
const raw = process.env[MODEL_CONTEXT_WINDOWS_ENV_KEY]
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
if (!raw?.trim()) {
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const windows: Record<string, number> = {}
|
||||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
for (const [model, value] of Object.entries(parsed)) {
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
const normalized = normalizeWindow(value)
|
||||||
return {}
|
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 {
|
function findConfiguredModelContextWindow(
|
||||||
const configured = parseConfiguredContextWindows()
|
model: string,
|
||||||
|
configured: Record<string, number>,
|
||||||
|
): number | undefined {
|
||||||
const normalizedModel = normalizeModelContextKey(model)
|
const normalizedModel = normalizeModelContextKey(model)
|
||||||
const exact = configured[normalizedModel]
|
const exact = configured[normalizedModel]
|
||||||
if (exact !== undefined) {
|
if (exact !== undefined) {
|
||||||
@ -105,6 +97,42 @@ function getConfiguredModelContextWindow(model: string): number | undefined {
|
|||||||
return 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 {
|
function getBuiltInModelContextWindow(model: string): number | undefined {
|
||||||
const normalizedModel = normalizeModelContextKey(model)
|
const normalizedModel = normalizeModelContextKey(model)
|
||||||
const exact = DIRECT_MODEL_CONTEXT_WINDOWS[normalizedModel]
|
const exact = DIRECT_MODEL_CONTEXT_WINDOWS[normalizedModel]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user