Skip to content

Commit e932bbe

Browse files
author
weiesky.wangc
committed
feat(context): kimi logo refresh + context-window follows hot-switched model
- ui(avatar): refresh Kimi logo — brand-blue dot fixed (#1783FF), "K" glyph switched to currentColor so it inverts with --model-logo-mono (white in dark, black in light), matching GLM/MiniMax mono-logo theming. - feat(context): MODEL_CONTEXT_SIZES adds /kimi|moonshot|^k3$/i → 256K (Kimi's real window); classifyContextWindow buckets kimi/moonshot-prefixed names and bare k3 into the 1M bar bucket. [Nk]/[Nm] suffix still overrides everything. - fix(context): SSE context_window window now follows the hot-switched upstream model — getContextSizeForModel accepts a log entry and prefers the response model over the original request name, skipping the stale startup cache; adaptContextWindow gains a 256K→1M correction tier. - fix(context): new getCalibrationModel (context-rules.js, shared src/server) gives the request-side explicit [Nk]/[Nm] config suffix precedence over upstream model normalization (Moonshot returns bare "k3" for k3[1m]), so the bar's denominator honors the configured 1M intent on both frontend (resolveCalibrationTokens) and server (getContextSizeForModel) paths. Release 1.7.12.
1 parent 82db2b9 commit e932bbe

14 files changed

Lines changed: 290 additions & 15 deletions

history.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Changelog
22

3-
## Unreleased
3+
## 1.7.12 (2026-07-30)
4+
5+
- ui(avatar): **refresh the Kimi model logo** — new mark with the brand-blue dot kept fixed (`#1783FF`) and the "K" glyph switched to `currentColor` so it follows `--model-logo-mono` (white in dark mode, black in light mode), matching the GLM/MiniMax mono-logo theming.
6+
7+
- feat(context): **Kimi models no longer forced into the 200K bucket**`MODEL_CONTEXT_SIZES` gains a `/kimi|moonshot|^k3$/i → 256K` entry (Kimi's real window), so the server-computed `context_window_size` reflects the correct 256K scale; for the user-visible bar, `classifyContextWindow` buckets kimi/moonshot-prefixed models **and bare `k3`** into 1M (avoids a mid-session 200K→1M rescale; the bar simply under-reads against the true 256K ceiling). Bare `k3` must be 1M because hot-switching to `k3[1m]` makes the upstream strip the suffix and return `response.body.model: "k3"` — the response-first resolver would otherwise split the bar's denominator (200K) from the request side (1M). The `[Nk]/[Nm]` suffix still overrides everything.
8+
9+
- fix(context): **context bar window follows the hot-switched upstream model** — the SSE `context_window` producers (`log-watcher` live path and `/events` cold-load) sized the window from the request model (`body.model`), which under proxy hot-switch is the original client model, not the real upstream one. `getContextSizeForModel` now accepts a log entry and prefers `response.body.model` (same precedence as the frontend's `getEffectiveModel`), skipping the stale startup cache on that path; `adaptContextWindow` gains a 256K→1M correction tier so an over-window kimi reading no longer pins the bar at 100%.
10+
11+
- fix(context): **explicit `[Nk]/[Nm]` config suffix wins over upstream model normalization** — hot-switching to `k3[1m]` makes Moonshot return `response.body.model: "k3"` (the `[1m]` context marker is client-side and not echoed back); the response-first resolver then misread the window as 200K (bar showed 76% instead of 15%). New `getCalibrationModel` (context-rules.js, shared src/server) resolves the calibration model with the request-side explicit suffix taking precedence over the upstream-normalized response, falling back to response-first otherwise; both `resolveCalibrationTokens` (frontend) and `getContextSizeForModel` (server SSE) now use it, so the bar's denominator honors the configured 1M intent.
412

513
## 1.7.11 (2026-07-29)
614

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cc-viewer",
3-
"version": "1.7.11",
3+
"version": "1.7.12",
44
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
55
"license": "MIT",
66
"main": "server.js",

server/lib/context-rules.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,29 @@ export function parseContextSizeSuffix(modelName) {
2727
return m[2].toLowerCase() === 'm' ? num * 1000000 : num * 1000;
2828
}
2929

30+
/**
31+
* Resolve the model name to use for context-window classification (血条窗口判定专用).
32+
*
33+
* Precedence differs from getEffectiveModel (response-first) on one deliberate
34+
* point: an EXPLICIT [Nk]/[Nm] suffix on the REQUEST model (`body.model`, which
35+
* carries the user's hot-switch config / model selector intent) is authoritative
36+
* and must NOT be overridden by the upstream response. Upstream APIs normalize
37+
* the response `model` — e.g. hot-switching to `k3[1m]` makes Moonshot return
38+
* `response.body.model: "k3"`, stripping the [1m] marker; a response-first read
39+
* would then misclassify the window (bare k3 vs the configured 1M). So: request
40+
* suffix wins; otherwise fall back to the response model, then the request name.
41+
*
42+
* @param {object|null|undefined} request log entry with body / response
43+
* @returns {string|null}
44+
*/
45+
export function getCalibrationModel(request) {
46+
const reqModel = request?.body?.model;
47+
if (typeof reqModel === 'string' && parseContextSizeSuffix(reqModel) != null) return reqModel;
48+
const respModel = request?.response?.body?.model;
49+
if (typeof respModel === 'string' && respModel) return respModel;
50+
return (typeof reqModel === 'string' && reqModel) ? reqModel : null;
51+
}
52+
3053
// 模型家族 → 窗口档位表(有序,首条命中)。后缀解析在表外先行(见 getModelMaxTokens)。
3154
const MODEL_CONTEXT_SIZES = [
3255
// haiku 全系 200K,显式置于一切 1M 默认之前(claude-haiku-4-5 等)
@@ -48,6 +71,10 @@ const MODEL_CONTEXT_SIZES = [
4871
{ match: /gpt-4o|o1|o3|o4/i, tokens: 128000 },
4972
{ match: /gpt-4/i, tokens: 128000 },
5073
{ match: /gpt-3/i, tokens: 16000 },
74+
// Kimi 家族精确档:k2.x/k3 等带 kimi/moonshot 前缀的 → 256K;裸 'k3'(无前缀,
75+
// 代理直连时的简写 model 名)→ 256K 精确档但 classifyContextWindow 不升 1M
76+
// (见该函数的家族特判,裸 k3 归 200K 桶,超量由 adaptContextWindow 纠偏)。
77+
{ match: /kimi|moonshot|^k3$/i, tokens: 256000 },
5178
// deepseek-v4 defaults to 1M; placed before generic /deepseek/ so the
5279
// first-match-wins loop picks it up before falling through to 128K.
5380
{ match: /deepseek-v4/i, tokens: 1000000 },
@@ -74,12 +101,19 @@ export function getModelMaxTokens(modelName) {
74101
* 不变量:只返回 1000000 或 200000(resolveCalibrationTokens 依赖此不变量)。
75102
* 裸 '1m' 子串(无方括号,如 deepseek-v3-1m)→ 1M 的宽松规则仅限本分类器,
76103
* 刻意不进 getModelMaxTokens(后者面向精确档位)。128K/16K 档归入 200K 桶。
104+
* Kimi 家族特判:kimi/moonshot 前缀型号(k2.x/k3,真实窗口 256K)归 1M 桶 ——
105+
* 避免会话中段从 200K 重标定到 256K/1M 的跳变;代价是相对真实 256K 上限
106+
* 长期低估(约 4 倍刻度),可接受。裸 'k3' 同样归 1M:代理热切换到
107+
* 'k3[1m]' 时上游会把响应 model 归一化成裸 'k3'(剥掉 [1m] 后缀),
108+
* response-first 解析读到裸 'k3' 若归 200K 桶会与请求侧 1M 判定分裂,
109+
* 血条分母错成 200K;且裸 'k3' 本就是 k3[1m] 的 1M 形态被剥后缀的产物。
77110
* @param {string} modelName
78111
* @returns {1000000|200000}
79112
*/
80113
export function classifyContextWindow(modelName) {
81114
if (!modelName || typeof modelName !== 'string') return 200000;
82115
if (modelName.toLowerCase().includes('1m')) return 1000000;
116+
if (/kimi|moonshot|^k3$/i.test(modelName)) return 1000000;
83117
return getModelMaxTokens(modelName) >= 1000000 ? 1000000 : 200000;
84118
}
85119

@@ -88,7 +122,9 @@ export function classifyContextWindow(modelName) {
88122
* 一个真正的 200K 模型,其输入上下文(input + cache_creation + cache_read)物理上不可能
89123
* 超过 200K —— 超了 API 直接拒收。所以一旦真实输入用量越过 200K 还被判成 200K,必然是
90124
* model 名识别错了(误判),此时自动升到 1M,免得血条卡死在 100%、百分比与真实进度脱节。
91-
* 仅做 200K→1M 这一个方向的纠偏;其余判定(1M、各家 200K 真值等)一律原样返回。
125+
* One-way upgrades only: 200K→1M and 256K→1M (the kimi exact tier used by the
126+
* server-side SSE path); every other classification (1M, 128K/16K tiers, true
127+
* 200K values) is returned unchanged — 128K is deliberately never promoted.
92128
* 注意:usedContextTokens 必须是"输入侧"用量(sumUsageInputTokens,不含 output_tokens),
93129
* 否则大输出会误触发。
94130
* @param {number} classifiedTokens classifyContextWindow / getModelMaxTokens 的结果
@@ -97,6 +133,7 @@ export function classifyContextWindow(modelName) {
97133
*/
98134
export function adaptContextWindow(classifiedTokens, usedContextTokens) {
99135
if (classifiedTokens === 200000 && usedContextTokens > 200000) return 1000000;
136+
if (classifiedTokens === 256000 && usedContextTokens > 256000) return 1000000;
100137
return classifiedTokens;
101138
}
102139

server/lib/context-watcher.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { readFileSync, existsSync, realpathSync } from 'node:fs';
22
import { join } from 'node:path';
33
import { homedir } from 'node:os';
44
import { getClaudeConfigDir } from '../../findcc.js';
5-
import { getModelMaxTokens, adaptContextWindow, sumUsageInputTokens, sumUsageContextTokens } from './context-rules.js';
5+
import { getModelMaxTokens, adaptContextWindow, sumUsageInputTokens, sumUsageContextTokens, getCalibrationModel } from './context-rules.js';
66

77
export const CONTEXT_WINDOW_FILE = join(getClaudeConfigDir(), 'context-window.json');
88
export const CLAUDE_SETTINGS_FILE = join(getClaudeConfigDir(), 'settings.json');
@@ -43,10 +43,25 @@ export function readModelContextSize() {
4343
/**
4444
* Get context size for a given API model name (e.g. 'claude-opus-4-6-20250514').
4545
* Uses startup cache to avoid re-reading the file.
46-
* @param {string} apiModelName - model name from req.body.model
46+
* Accepts either a bare model-name string (legacy path) or a full log entry.
47+
* Entry input resolves the model via getCalibrationModel (context-rules.js):
48+
* an explicit [Nk]/[Nm] suffix on the REQUEST model wins (the user's hot-switch
49+
* config intent, e.g. k3[1m]); otherwise the upstream response.body.model is
50+
* authoritative. The startup cache is request-side static info, stale after a
51+
* hot-switch, so entry resolution skips it and goes straight to the family
52+
* rules table. String input keeps legacy cache-first behavior unchanged.
53+
* @param {string|object} modelOrEntry - model name, or log entry with body/response
4754
* @returns {number} context window size in tokens
4855
*/
49-
export function getContextSizeForModel(apiModelName) {
56+
export function getContextSizeForModel(modelOrEntry) {
57+
const isEntry = modelOrEntry !== null && typeof modelOrEntry === 'object';
58+
// Entry input: calibration-aware resolution (request [Nk]/[Nm] suffix wins,
59+
// else response model). Authoritative over the stale startup cache.
60+
if (isEntry) {
61+
const model = getCalibrationModel(modelOrEntry);
62+
return model ? getModelMaxTokens(model) : (_startupContextSize || 200000);
63+
}
64+
const apiModelName = modelOrEntry;
5065
if (!apiModelName) return _startupContextSize || 200000;
5166
const lower = apiModelName.toLowerCase();
5267
// Extract base: 'claude-opus-4-6-20250514' → 'opus-4-6'
@@ -56,7 +71,7 @@ export function getContextSizeForModel(apiModelName) {
5671
return _startupContextSize;
5772
}
5873
// 完整档位表见 server/lib/context-rules.js(与前端同源;含 haiku/旧 opus/3-opus 200K、
59-
// deepseek-v4 1M、gpt/deepseek 等三方档位,默认 200K)
74+
// deepseek-v4 1M、kimi/moonshot 256K、gpt/deepseek 等三方档位,默认 200K)
6075
return getModelMaxTokens(apiModelName);
6176
}
6277

server/lib/log-watcher.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function processWatchedEntry(parsed, ctx) {
156156
if (cached) sendEventToClients(clients, 'kv_cache_content', cached);
157157
const usage = parsed.response?.body?.usage;
158158
if (usage) {
159-
const contextSize = getContextSizeForModel(parsed.body?.model);
159+
const contextSize = getContextSizeForModel(parsed);
160160
const cwData = buildContextWindowEvent(usage, contextSize);
161161
if (cwData) sendEventToClients(clients, 'context_window', cwData);
162162
}

server/routes/events.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
320320
if (!latestContextWindow) {
321321
const usage = entry.response?.body?.usage;
322322
if (usage) {
323-
const contextSize = getContextSizeForModel(entry.body?.model);
323+
const contextSize = getContextSizeForModel(entry);
324324
const cw = buildContextWindowEvent(usage, contextSize);
325325
if (cw) latestContextWindow = cw;
326326
}

src/img/model-kimi.svg

Lines changed: 1 addition & 1 deletion
Loading

src/utils/effectiveModel.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* server-reported model in `response.body.model` (authoritative under proxy
88
* hot-switch) over the client-supplied `body.model`. Returns null when both
99
* are missing — callers should fall back to a sensible default.
10+
*
11+
* KEEP IN SYNC: server/lib/context-watcher.js getContextSizeForModel reuses
12+
* this precedence for its entry path — changing the priority here must be
13+
* mirrored there (and vice versa).
1014
*/
1115
export function getEffectiveModel(request) {
1216
return request?.response?.body?.model || request?.body?.model || null;

src/utils/helpers.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export {
2020
sumCacheCreationTokens,
2121
sumUsageInputTokens,
2222
sumUsageContextTokens,
23+
getCalibrationModel,
2324
} from '../../server/lib/context-rules.js';
24-
import { classifyContextWindow, adaptContextWindow } from '../../server/lib/context-rules.js';
25+
import { classifyContextWindow, adaptContextWindow, getCalibrationModel } from '../../server/lib/context-rules.js';
2526

2627
// getEffectiveModel moved to ./effectiveModel.js (pure, node-testable — sessionMerge/sessionManager
2728
// import it without helpers' Vite-only svg imports); re-exported here to keep import paths stable.
@@ -57,7 +58,9 @@ const CALIBRATION_TOKEN_MAP = {
5758
export function resolveCalibrationTokens(calibrationModel, lastMainAgent, projectModelHint = null) {
5859
const direct = CALIBRATION_TOKEN_MAP[calibrationModel];
5960
if (direct) return direct;
60-
const lastModel = lastMainAgent ? getEffectiveModel(lastMainAgent) : null;
61+
// 校准用 getCalibrationModel:请求名带显式 [Nk]/[Nm] 后缀时优先(用户热切换配置的
62+
// 1M 意图),不被上游响应归一化(如 k3[1m]→裸 k3)覆盖;其余回退 response-first。
63+
const lastModel = lastMainAgent ? getCalibrationModel(lastMainAgent) : null;
6164
// 优先用真实 mainAgent 信号;haiku 一律视为 init ping 噪声,跳过
6265
if (typeof lastModel === 'string' && lastModel && !/haiku/i.test(lastModel)) {
6366
return classifyContextWindow(lastModel);
@@ -334,7 +337,7 @@ const MODEL_PROVIDERS = [
334337
match: /kimi|moonshot|^k3$/i,
335338
name: 'Kimi',
336339
color: 'var(--bg-model-avatar)',
337-
svg: '<svg t="1771495664798" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6649" width="200" height="200"><path d="M731.062857 590.262857v318.317714h-148.589714V443.977143a146.285714 146.285714 0 0 1-146.285714 146.285714l-214.491429-0.036571v318.354285H73.142857V165.814857h148.553143v275.858286h180.882286l119.771428-275.858286h167.753143l-67.84 156.269714a255.524571 255.524571 0 0 1-106.678857 119.588572h66.925714a148.553143 148.553143 0 0 1 148.516572 148.589714z m120.758857-473.965714a99.035429 99.035429 0 0 1 0 198.070857h-99.035428V215.332571a99.035429 99.035429 0 0 1 99.035428-99.035428z" fill="currentColor" p-id="6650"></path></svg>',
340+
svg: '<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M932.096 0a82.048 82.048 0 1 1 0 164.096h-72.352a9.568 9.568 0 0 1-9.664-9.632V82.016A82.048 82.048 0 0 1 932.096 0z" fill="#1783FF"></path><path d="M472.064 477.856l309.76-307.2c5.888-5.792 2.56-17.472-4.96-17.472h-166.72a7.008 7.008 0 0 0-5.056 2.112L271.456 486.24c-5.12 5.12-12.8 0.576-12.8-7.68V162.976c0-5.376-3.584-9.792-7.936-9.792H135.936c-4.352 0-7.936 4.416-7.936 9.792V843.52c0 5.44 3.584 9.824 7.936 9.824h114.784c4.352 0 7.936-4.288 7.936-9.824v-138.656c0-2.912 1.024-5.728 2.88-7.616l103.424-102.656a6.72 6.72 0 0 1 8.8-0.928l276.64 203.616a328.64 328.64 0 0 0 147.296 54.688c4.608 0.512 8.544-4 8.544-9.824V711.68c0-4.96-2.912-9.056-7.008-9.632a214.528 214.528 0 0 1-86.432-34.496l-239.456-173.472c-5.024-3.328-5.632-11.936-1.184-16.224h-0.096z" fill="currentColor"></path></svg>',
338341
},
339342
{
340343
match: /glm|chatglm/i,

0 commit comments

Comments
 (0)