thatgfsj-code 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.d.ts +6 -0
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +8 -0
- package/dist/app/index.js.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/smartModel.d.ts +68 -20
- package/dist/cache/smartModel.d.ts.map +1 -1
- package/dist/cache/smartModel.js +98 -20
- package/dist/cache/smartModel.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -4
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +5 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +17 -0
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +25 -1
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/index.d.ts +9 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +36 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/provider.d.ts +6 -1
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +17 -44
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.d.ts +7 -0
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +2 -2
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/InitWizard.d.ts +18 -9
- package/dist/tui/components/InitWizard.d.ts.map +1 -1
- package/dist/tui/components/InitWizard.js +76 -52
- package/dist/tui/components/InitWizard.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +38 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/package.json +1 -1
- package/src/app/index.ts +8 -0
- package/src/cache/index.ts +7 -1
- package/src/cache/smartModel.ts +113 -20
- package/src/config/index.ts +6 -4
- package/src/config/types.ts +5 -1
- package/src/llm/anthropic.ts +26 -1
- package/src/llm/index.ts +38 -1
- package/src/llm/provider.ts +6 -1
- package/src/tui/app.tsx +15 -56
- package/src/tui/components/Header.tsx +12 -2
- package/src/tui/components/InitWizard.tsx +103 -69
- package/src/tui/hooks/useCommands.ts +39 -0
- package/tests/cache/smartModel.test.ts +112 -0
|
@@ -12,6 +12,8 @@ const CMD_ALIASES = {
|
|
|
12
12
|
'/服务商': '/provider',
|
|
13
13
|
'/思考': '/thinking',
|
|
14
14
|
'/缓存': '/cache',
|
|
15
|
+
'/ttl': '/ttl',
|
|
16
|
+
'/TTL': '/ttl',
|
|
15
17
|
};
|
|
16
18
|
export const COMMAND_LIST = [
|
|
17
19
|
{ name: '/模型', desc: '切换模型' },
|
|
@@ -19,6 +21,7 @@ export const COMMAND_LIST = [
|
|
|
19
21
|
{ name: '/新建', desc: '新建会话' },
|
|
20
22
|
{ name: '/压缩', desc: '压缩上下文' },
|
|
21
23
|
{ name: '/缓存', desc: '缓存命中率' },
|
|
24
|
+
{ name: '/ttl', desc: '查看/设置 Cache TTL' },
|
|
22
25
|
{ name: '/技能', desc: '管理技能' },
|
|
23
26
|
{ name: '/mcp', desc: 'MCP 设置' },
|
|
24
27
|
{ name: '/帮助', desc: '查看帮助' },
|
|
@@ -124,6 +127,41 @@ export function useCommands(app) {
|
|
|
124
127
|
lines.push('/cache reset · 重置统计');
|
|
125
128
|
return { handled: true, output: lines.join('\n') };
|
|
126
129
|
}
|
|
130
|
+
// v3.0.3: /ttl — view or pin the cache TTL.
|
|
131
|
+
//
|
|
132
|
+
// /ttl show current effective TTL + decision reason
|
|
133
|
+
// /ttl 5m|1h pin TTL for this session (sticky, no cache reset)
|
|
134
|
+
// /ttl auto resume auto-decision (clears the pin)
|
|
135
|
+
if (name === '/ttl') {
|
|
136
|
+
const configTtl = app.config.get().cache?.ttl;
|
|
137
|
+
const resolved = app.resolvedTtl;
|
|
138
|
+
if (arg === 'auto') {
|
|
139
|
+
app.config.save({ cache: { ...app.config.get().cache, ttl: 'auto' } });
|
|
140
|
+
app.resolvedTtl = null;
|
|
141
|
+
return { handled: true, output: '✓ Cache TTL 已重置为自动(下一轮重新评估)' };
|
|
142
|
+
}
|
|
143
|
+
if (arg === '5m' || arg === '1h') {
|
|
144
|
+
app.config.save({ cache: { ...app.config.get().cache, ttl: arg } });
|
|
145
|
+
app.resolvedTtl = arg;
|
|
146
|
+
return { handled: true, output: `✓ Cache TTL 已固定为 ${arg}(首次请求会重建缓存)` };
|
|
147
|
+
}
|
|
148
|
+
// No arg: show current state
|
|
149
|
+
const lines = [
|
|
150
|
+
'⏱ Cache TTL',
|
|
151
|
+
'─────────────────────────',
|
|
152
|
+
` 配置: ${configTtl ?? 'auto'}`,
|
|
153
|
+
` 当前会话: ${resolved ?? '尚未评估'}`,
|
|
154
|
+
'',
|
|
155
|
+
'说明: ' +
|
|
156
|
+
(configTtl === 'auto'
|
|
157
|
+
? '自动模式 – 短会话(≤14 轮)用 5m节省写入成本,' +
|
|
158
|
+
'长会话(≥15 轮 或 ≥50k 字符)自动升级到 1h 复用缓存。'
|
|
159
|
+
: `${configTtl} 模式 – TTL 每次请求都固定。`),
|
|
160
|
+
'',
|
|
161
|
+
'/ttl 5m|1h|auto 调整 TTL',
|
|
162
|
+
];
|
|
163
|
+
return { handled: true, output: lines.join('\n') };
|
|
164
|
+
}
|
|
127
165
|
// ── /skills [id] ────────────────────────────────────
|
|
128
166
|
if (name === '/skills') {
|
|
129
167
|
if (arg) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AASpC,WAAW;AACX,MAAM,WAAW,GAA2B;IAC1C,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,UAAU;IACjB,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,WAAW;IACnB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,QAAQ;
|
|
1
|
+
{"version":3,"file":"useCommands.js","sourceRoot":"","sources":["../../../src/tui/hooks/useCommands.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/app/index.ts
CHANGED
|
@@ -38,6 +38,12 @@ export class App {
|
|
|
38
38
|
* in the REPL.
|
|
39
39
|
*/
|
|
40
40
|
showThinking: boolean = false;
|
|
41
|
+
/**
|
|
42
|
+
* v3.0.3: TTL resolved per session. null = not yet decided (auto mode
|
|
43
|
+
* waiting for first round). After the first chatStream completes, this
|
|
44
|
+
* is '5m' or '1h' and stays sticky.
|
|
45
|
+
*/
|
|
46
|
+
resolvedTtl: '5m' | '1h' | null = null;
|
|
41
47
|
|
|
42
48
|
private constructor(
|
|
43
49
|
config: ConfigManager,
|
|
@@ -117,6 +123,8 @@ export class App {
|
|
|
117
123
|
const msgs = messages || this.session.getMessages();
|
|
118
124
|
const inner = this.llm.chatStream(msgs);
|
|
119
125
|
const debugUsage = !!process.env.GFCODE_DEBUG_USAGE;
|
|
126
|
+
// v3.0.3: read TTL the LLMService resolved this round (sticky per session).
|
|
127
|
+
this.resolvedTtl = this.llm.getResolvedTTL();
|
|
120
128
|
let next = await inner.next();
|
|
121
129
|
while (!next.done) {
|
|
122
130
|
// Forward chunks unchanged, but capture usage into the persistent
|
package/src/cache/index.ts
CHANGED
|
@@ -12,4 +12,10 @@
|
|
|
12
12
|
export { fingerprint, fingerprintTools, fingerprintSystemPrefix } from './fingerprint.js';
|
|
13
13
|
export { CacheStatsStore, estimateSavingsCNY, type CacheStats, type CacheSnapshot } from './stats.js';
|
|
14
14
|
export { VolatileScratch } from './volatile.js';
|
|
15
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
shouldDowngrade,
|
|
17
|
+
decideTTL,
|
|
18
|
+
totalConversationChars,
|
|
19
|
+
type SmartModelDecision,
|
|
20
|
+
type SmartTTLDecision,
|
|
21
|
+
} from './smartModel.js';
|
package/src/cache/smartModel.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Smart-model routing (P3 of the Reasonix plan
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
2
|
+
* Smart-model routing + auto TTL selection (P3 of the Reasonix plan,
|
|
3
|
+
* v3.0.3 onwards).
|
|
4
|
+
*
|
|
5
|
+
* Idea: classify the conversation as "short" / "medium" / "long" and
|
|
6
|
+
* route accordingly. Two levers:
|
|
7
|
+
*
|
|
8
|
+
* 1. Model downgrade
|
|
9
|
+
* Short follow-ups ("yes", "thanks") → mini model
|
|
10
|
+
* Tool-heavy turns → main model
|
|
11
|
+
*
|
|
12
|
+
* 2. Cache TTL
|
|
13
|
+
* Short conversations → 5m (cheap write, expires fast)
|
|
14
|
+
* Long conversations (>15 turns) → 1h (cache hits pay back the
|
|
15
|
+
* expensive write each round)
|
|
16
|
+
*
|
|
17
|
+
* Why TTL is decided per-ROUND but CHANGES per-CONVERSATION:
|
|
18
|
+
* The Anthropic cache_control marker is part of the request body.
|
|
19
|
+
* Changing the TTL between rounds would push the wire bytes to a
|
|
20
|
+
* different Anthropic cache bucket, blowing the cache on every round
|
|
21
|
+
* and *costing* more in cache_creation_input_tokens than what we save.
|
|
22
|
+
*
|
|
23
|
+
* So we compute the TTL once per conversation (lazily, on the first
|
|
24
|
+
* round) and never change it. Subsequent rounds get the same TTL,
|
|
25
|
+
* which keeps the cache prefix stable and lets Anthropic match it.
|
|
26
|
+
*
|
|
27
|
+
* The "smart" part is that we look at the FULL conversation so far,
|
|
28
|
+
* not just the last input. A 16-turn conversation that started with a
|
|
29
|
+
* short prompt but grew into a long refactor still gets 1h.
|
|
30
|
+
*
|
|
31
|
+
* Trade-off: the first round is evaluated with 0 history, so it
|
|
32
|
+
* defaults to 5m. If the conversation then grows long, future rounds
|
|
33
|
+
* still use 5m for the system-token cache (because we already wrote
|
|
34
|
+
* it with 5m). The tool definition cache gets the same TTL.
|
|
22
35
|
*/
|
|
23
36
|
|
|
24
37
|
import type { ChatMessage } from '../types.js';
|
|
@@ -30,6 +43,13 @@ export interface SmartModelDecision {
|
|
|
30
43
|
reason: string;
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
export interface SmartTTLDecision {
|
|
47
|
+
ttl: '5m' | '1h';
|
|
48
|
+
reason: string;
|
|
49
|
+
/** Computed per-conversation. Once we pick a TTL, we keep it. */
|
|
50
|
+
isFirstDecision: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
/**
|
|
34
54
|
* Heuristic: downgrade when
|
|
35
55
|
* 1. The latest user message is short (< 200 chars), AND
|
|
@@ -50,3 +70,76 @@ export function shouldDowngrade(messages: ChatMessage[], lastUserInput: string):
|
|
|
50
70
|
}
|
|
51
71
|
return { downgrade: true, reason: 'short-no-tools' };
|
|
52
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Total character count of all message content. Used as a cheap
|
|
76
|
+
* proxy for "is this conversation long enough to justify 1h TTL?"
|
|
77
|
+
* (Anthropic charges cache_creation at 1.25x normal input, so we
|
|
78
|
+
* need to be sure the conversation will hit cache ~10+ times to
|
|
79
|
+
* break even on 1h vs 5m.)
|
|
80
|
+
*/
|
|
81
|
+
export function totalConversationChars(messages: ChatMessage[]): number {
|
|
82
|
+
let total = 0;
|
|
83
|
+
for (const m of messages) {
|
|
84
|
+
if (typeof m.content === 'string') {
|
|
85
|
+
total += m.content.length;
|
|
86
|
+
} else if (Array.isArray(m.content)) {
|
|
87
|
+
for (const blk of m.content) {
|
|
88
|
+
if (blk.type === 'text') total += (blk as any).text.length;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return total;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decide the cache TTL for the current round.
|
|
97
|
+
*
|
|
98
|
+
* Inputs:
|
|
99
|
+
* - messages: full conversation so far (system + user + assistant + tool)
|
|
100
|
+
* - previousTTL: TTL chosen in a prior round (null on round 0)
|
|
101
|
+
*
|
|
102
|
+
* Decision matrix:
|
|
103
|
+
* - round 0 + any input length → 5m (default; user can override via init)
|
|
104
|
+
* - round 1-14 + total chars < 50k → 5m (short sessions, 5m is enough)
|
|
105
|
+
* - round 15+ OR total chars > 50k → 1h (long sessions, 1h amortizes the write)
|
|
106
|
+
*
|
|
107
|
+
* Stability rule: once a TTL is chosen, it is reused for every
|
|
108
|
+
* subsequent round in the same session. Changing TTL between rounds
|
|
109
|
+
* would invalidate the Anthropic cache prefix and cost more than it
|
|
110
|
+
* saves.
|
|
111
|
+
*
|
|
112
|
+
* The `isFirstDecision` flag tells the caller whether to apply the
|
|
113
|
+
* decision (first round) or reuse the previous one (later rounds).
|
|
114
|
+
*/
|
|
115
|
+
export function decideTTL(
|
|
116
|
+
messages: ChatMessage[],
|
|
117
|
+
previousTTL: '5m' | '1h' | null,
|
|
118
|
+
): SmartTTLDecision {
|
|
119
|
+
if (previousTTL) {
|
|
120
|
+
return { ttl: previousTTL, reason: 'reused-from-previous-round', isFirstDecision: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const turnCount = messages.filter(m => m.role === 'user' || m.role === 'assistant').length;
|
|
124
|
+
const totalChars = totalConversationChars(messages);
|
|
125
|
+
|
|
126
|
+
// Round 0: empty / first input. Default to 5m.
|
|
127
|
+
if (turnCount === 0) {
|
|
128
|
+
return { ttl: '5m', reason: 'first-round-default', isFirstDecision: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Multi-turn but small conversation → 5m
|
|
132
|
+
if (turnCount < 15 && totalChars < 50_000) {
|
|
133
|
+
return { ttl: '5m', reason: `short-session-${turnCount}-turns`, isFirstDecision: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Long conversations → 1h
|
|
137
|
+
if (turnCount >= 15) {
|
|
138
|
+
return { ttl: '1h', reason: `long-session-${turnCount}-turns`, isFirstDecision: true };
|
|
139
|
+
}
|
|
140
|
+
if (totalChars >= 50_000) {
|
|
141
|
+
return { ttl: '1h', reason: `long-context-${totalChars}-chars`, isFirstDecision: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { ttl: '5m', reason: 'fallback', isFirstDecision: true };
|
|
145
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -16,12 +16,14 @@ const DEFAULT_CONFIG: Config = {
|
|
|
16
16
|
maxTokens: 4096,
|
|
17
17
|
contextLength: 50,
|
|
18
18
|
provider: 'siliconflow',
|
|
19
|
-
// v3.0.0: prompt cache policy. Default = enabled,
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// v3.0.0: prompt cache policy. Default = enabled, 'auto' TTL.
|
|
20
|
+
// v3.0.3: 'auto' TTL lets the runtime's decideTTL() pick 5m for short
|
|
21
|
+
// sessions and 1h for long ones (>= 15 turns or >= 50k chars). The
|
|
22
|
+
// chosen TTL is sticky across rounds within a session so the cache
|
|
23
|
+
// prefix stays stable.
|
|
22
24
|
cache: {
|
|
23
25
|
enabled: true,
|
|
24
|
-
ttl: '
|
|
26
|
+
ttl: 'auto',
|
|
25
27
|
strategy: 'auto',
|
|
26
28
|
},
|
|
27
29
|
};
|
package/src/config/types.ts
CHANGED
|
@@ -43,10 +43,14 @@ export interface Config {
|
|
|
43
43
|
* Note: enabling/disabling only affects Anthropic; other providers do
|
|
44
44
|
* not expose a programmatic cache toggle, so the flag is purely
|
|
45
45
|
* advisory there.
|
|
46
|
+
*
|
|
47
|
+
* v3.0.3: `ttl` accepts '5m' | '1h' | 'auto'. 'auto' lets the runtime
|
|
48
|
+
* pick the TTL based on conversation length (decideTTL in smartModel.ts).
|
|
49
|
+
* The wizard's default is 'auto' so users don't have to choose.
|
|
46
50
|
*/
|
|
47
51
|
cache?: {
|
|
48
52
|
enabled: boolean;
|
|
49
|
-
ttl?: '5m' | '1h';
|
|
53
|
+
ttl?: '5m' | '1h' | 'auto';
|
|
50
54
|
strategy?: 'auto' | 'manual' | 'off';
|
|
51
55
|
};
|
|
52
56
|
}
|
package/src/llm/anthropic.ts
CHANGED
|
@@ -46,11 +46,31 @@ const DEFAULT_CACHE_TTL: '5m' | '1h' = '5m';
|
|
|
46
46
|
export class AnthropicProvider implements LLMProvider {
|
|
47
47
|
readonly name = 'anthropic';
|
|
48
48
|
protected config: ProviderConfig;
|
|
49
|
+
/**
|
|
50
|
+
* v3.0.3: TTL resolved from the user's config + smart routing. This is
|
|
51
|
+
* the *actual* TTL attached to every cache_control marker we send this
|
|
52
|
+
* session. Stays stable across rounds to keep the Anthropic cache
|
|
53
|
+
* prefix intact.
|
|
54
|
+
*
|
|
55
|
+
* Computed once by LLMService.chatStream when ttl='auto', then
|
|
56
|
+
* persisted via setResolvedTTL. Subsequent rounds call chatStream
|
|
57
|
+
* without re-deciding (ChatOptions.resolvedTtl is sticky per session).
|
|
58
|
+
*/
|
|
59
|
+
protected resolvedTtl: '5m' | '1h' = DEFAULT_CACHE_TTL;
|
|
49
60
|
|
|
50
61
|
constructor(config: ProviderConfig) {
|
|
51
62
|
this.config = config;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Called by LLMService after decideTTL(). Persists the TTL on the
|
|
67
|
+
* provider instance so buildRequest() writes it consistently across
|
|
68
|
+
* every round in this session.
|
|
69
|
+
*/
|
|
70
|
+
setResolvedTTL(ttl: '5m' | '1h'): void {
|
|
71
|
+
this.resolvedTtl = ttl;
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
buildTools(tools: Tool[]): any[] {
|
|
55
75
|
return tools.map(tool => ({
|
|
56
76
|
name: tool.name,
|
|
@@ -247,7 +267,12 @@ export class AnthropicProvider implements LLMProvider {
|
|
|
247
267
|
// final block (and only the final block, per Anthropic convention)
|
|
248
268
|
// carries the cache_control marker.
|
|
249
269
|
const systemMessages = messages.filter(m => m.role === 'system');
|
|
250
|
-
|
|
270
|
+
// v3.0.3: TTL is sticky per session. The provider's resolvedTtl is
|
|
271
|
+
// set once by LLMService after decideTTL() and never changed within
|
|
272
|
+
// a session (changing it would invalidate the Anthropic cache
|
|
273
|
+
// prefix and cost more in cache_creation_input_tokens than it
|
|
274
|
+
// saves).
|
|
275
|
+
const cacheTtl = this.resolvedTtl;
|
|
251
276
|
const cacheEnabled = this.config.cache?.enabled !== false; // default on for Anthropic
|
|
252
277
|
|
|
253
278
|
const systemBlocks = systemMessages.length === 0 ? undefined : systemMessages.map((m, i, arr) => {
|
package/src/llm/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { PROVIDERS } from '../config/providers.js';
|
|
|
20
20
|
import { OpenAIProvider } from './openai.js';
|
|
21
21
|
import { AnthropicProvider } from './anthropic.js';
|
|
22
22
|
import { GeminiProvider } from './gemini.js';
|
|
23
|
+
import { decideTTL } from '../cache/smartModel.js';
|
|
23
24
|
|
|
24
25
|
export class LLMService {
|
|
25
26
|
private provider: LLMProvider;
|
|
@@ -31,6 +32,19 @@ export class LLMService {
|
|
|
31
32
|
this.apiKey = apiKey;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* v3.0.3: TTL resolved from config + smart routing. Stays null until
|
|
37
|
+
* the first round, then never changes for the session. Anthropic
|
|
38
|
+
* provider's setResolvedTTL is called at the same time, so the wire
|
|
39
|
+
* prefix is consistent across rounds.
|
|
40
|
+
*/
|
|
41
|
+
private resolvedTtl: '5m' | '1h' | null = null;
|
|
42
|
+
|
|
43
|
+
/** Public accessor used by App.streamResponse to surface TTL in the UI. */
|
|
44
|
+
getResolvedTTL(): '5m' | '1h' | null {
|
|
45
|
+
return this.resolvedTtl;
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
static fromConfig(config: AIConfig & { cache?: Config['cache'] }): LLMService {
|
|
35
49
|
const providerName = config.provider || 'siliconflow';
|
|
36
50
|
const providerConfig = PROVIDERS[providerName];
|
|
@@ -43,7 +57,9 @@ export class LLMService {
|
|
|
43
57
|
maxTokens: config.maxTokens ?? 4096,
|
|
44
58
|
// v3.0.0: forward cache policy. Anthropic reads this to decide
|
|
45
59
|
// whether to attach cache_control markers; other providers ignore it.
|
|
46
|
-
|
|
60
|
+
// v3.0.3: ttl accepts '5m' | '1h' | 'auto'. 'auto' is resolved
|
|
61
|
+
// per-session by decideTTL() inside chatStream.
|
|
62
|
+
cache: config.cache ?? { enabled: true, ttl: 'auto' as const, strategy: 'auto' as const },
|
|
47
63
|
};
|
|
48
64
|
|
|
49
65
|
const format = providerConfig.format;
|
|
@@ -98,6 +114,27 @@ export class LLMService {
|
|
|
98
114
|
): AsyncGenerator<StreamChunk, ChatResponse> {
|
|
99
115
|
if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
|
|
100
116
|
|
|
117
|
+
// v3.0.3: Resolve TTL once per session. If config says 'auto', call
|
|
118
|
+
// decideTTL to pick 5m (short sessions) or 1h (long ones). For
|
|
119
|
+
// non-Anthropic providers we don't need to do anything — cache
|
|
120
|
+
// markers are wired into the system regardless.
|
|
121
|
+
const configTtl = (this.provider as any).config?.cache?.ttl;
|
|
122
|
+
if (configTtl === 'auto' && this.resolvedTtl === null) {
|
|
123
|
+
const decision = decideTTL(messages, null);
|
|
124
|
+
this.resolvedTtl = decision.ttl;
|
|
125
|
+
if (typeof (this.provider as any).setResolvedTTL === 'function') {
|
|
126
|
+
(this.provider as any).setResolvedTTL(decision.ttl);
|
|
127
|
+
}
|
|
128
|
+
} else if (configTtl === '5m' || configTtl === '1h') {
|
|
129
|
+
// User pinned a specific TTL — apply it once and keep it.
|
|
130
|
+
if (this.resolvedTtl === null) {
|
|
131
|
+
this.resolvedTtl = configTtl;
|
|
132
|
+
if (typeof (this.provider as any).setResolvedTTL === 'function') {
|
|
133
|
+
(this.provider as any).setResolvedTTL(configTtl);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
const maxIterations = options?.maxIterations ?? 10;
|
|
102
139
|
let currentMessages = [...messages];
|
|
103
140
|
let iterations = 0;
|
package/src/llm/provider.ts
CHANGED
|
@@ -52,10 +52,15 @@ export interface ProviderConfig {
|
|
|
52
52
|
* Optional cache control policy. When omitted, providers fall back to their
|
|
53
53
|
* default behavior (Anthropic: explicit cache_control markers; OpenAI / Gemini
|
|
54
54
|
* / DeepSeek: automatic prefix cache, no markers needed).
|
|
55
|
+
*
|
|
56
|
+
* v3.0.3: `ttl` accepts '5m' | '1h' | 'auto'. 'auto' lets the runtime
|
|
57
|
+
* decide per-session via decideTTL() in cache/smartModel.ts. The actual
|
|
58
|
+
* value used at request time is what gets stored on the provider's
|
|
59
|
+
* resolvedTtl field (set by LLMService.chatStream).
|
|
55
60
|
*/
|
|
56
61
|
cache?: {
|
|
57
62
|
enabled: boolean;
|
|
58
|
-
ttl?: '5m' | '1h';
|
|
63
|
+
ttl?: '5m' | '1h' | 'auto';
|
|
59
64
|
/**
|
|
60
65
|
* 'auto' — provider default (Anthropic: explicit, OpenAI/DeepSeek: auto)
|
|
61
66
|
* 'manual' — always emit cache_control markers regardless of provider
|
package/src/tui/app.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import { useChat } from './hooks/useChat.js';
|
|
|
12
12
|
import { useCommands } from './hooks/useCommands.js';
|
|
13
13
|
import type { App } from '../app/index.js';
|
|
14
14
|
import type { MessageData } from './components/ChatMessage.js';
|
|
15
|
-
import type { ProviderName } from '../config/types.js';
|
|
16
15
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
17
16
|
import { join } from 'path';
|
|
18
17
|
import { homedir } from 'os';
|
|
@@ -35,15 +34,13 @@ function saveModelToHistory(model: string) {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
type ViewMode = 'chat' | 'model_select' | 'init_wizard'
|
|
37
|
+
type ViewMode = 'chat' | 'model_select' | 'init_wizard';
|
|
39
38
|
|
|
40
39
|
export function TuiApp({ app }: Props) {
|
|
41
40
|
const { messages, isThinking, streaming, streamingToolCalls, queuedMessage, sendMessage, cancel } = useChat(app);
|
|
42
41
|
const { handleCommand } = useCommands(app);
|
|
43
42
|
const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
|
|
44
43
|
const [viewMode, setViewMode] = useState<ViewMode>('chat');
|
|
45
|
-
const [initProvider, setInitProvider] = useState<ProviderName>('siliconflow');
|
|
46
|
-
const [initUrl, setInitUrl] = useState('');
|
|
47
44
|
const { stdout } = useStdout();
|
|
48
45
|
const terminalWidth = stdout?.columns || 80;
|
|
49
46
|
|
|
@@ -51,8 +48,14 @@ export function TuiApp({ app }: Props) {
|
|
|
51
48
|
// change (i.e. a round just completed and recorded new usage). Polling on
|
|
52
49
|
// an interval would be wasteful; React re-render is the trigger.
|
|
53
50
|
const [cacheSnapshot, setCacheSnapshot] = useState(() => app.cacheStats.snapshot());
|
|
51
|
+
// v3.0.3: resolved TTL (sticky per session). null until first round.
|
|
52
|
+
const [resolvedTtl, setResolvedTtl] = useState<'5m' | '1h' | null>(app.resolvedTtl);
|
|
53
|
+
// The user-pinned config TTL ('auto' | '5m' | '1h') — used to render the
|
|
54
|
+
// Header chip BEFORE the first round, when resolvedTtl is still null.
|
|
55
|
+
const configTtl = (app.config.get() as any).cache?.ttl as 'auto' | '5m' | '1h' | undefined;
|
|
54
56
|
useEffect(() => {
|
|
55
57
|
setCacheSnapshot(app.cacheStats.snapshot());
|
|
58
|
+
setResolvedTtl(app.resolvedTtl);
|
|
56
59
|
}, [messages.length]);
|
|
57
60
|
|
|
58
61
|
const addMsg = useCallback((content: string) => {
|
|
@@ -60,41 +63,6 @@ export function TuiApp({ app }: Props) {
|
|
|
60
63
|
}, []);
|
|
61
64
|
|
|
62
65
|
const onSubmit = useCallback(async (input: string) => {
|
|
63
|
-
// Init wizard - API Key input
|
|
64
|
-
if (viewMode === 'init_key') {
|
|
65
|
-
const key = input.trim();
|
|
66
|
-
if (!key) { setViewMode('chat'); return; }
|
|
67
|
-
// Save and complete
|
|
68
|
-
const { App: AppClass } = await import('../app/index.js');
|
|
69
|
-
const cfg = app.config.get();
|
|
70
|
-
app.config.save({ provider: initProvider, apiKey: key, model: cfg.model || 'default' });
|
|
71
|
-
saveModelToHistory(cfg.model || 'default');
|
|
72
|
-
setViewMode('chat');
|
|
73
|
-
addMsg(`配置已更新: ${initProvider}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Init wizard - Custom URL input
|
|
78
|
-
if (viewMode === 'init_url') {
|
|
79
|
-
const url = input.trim();
|
|
80
|
-
if (!url) { setViewMode('chat'); return; }
|
|
81
|
-
setInitUrl(url);
|
|
82
|
-
setViewMode('init_key');
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Init wizard - Custom model name
|
|
87
|
-
if (viewMode === 'init_custom_model') {
|
|
88
|
-
const model = input.trim();
|
|
89
|
-
if (model) {
|
|
90
|
-
app.config.save({ model });
|
|
91
|
-
saveModelToHistory(model);
|
|
92
|
-
addMsg(`模型已切换: ${model}`);
|
|
93
|
-
}
|
|
94
|
-
setViewMode('chat');
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
66
|
// Model selector - ignore text input
|
|
99
67
|
if (viewMode === 'model_select') return;
|
|
100
68
|
|
|
@@ -125,7 +93,7 @@ export function TuiApp({ app }: Props) {
|
|
|
125
93
|
}
|
|
126
94
|
|
|
127
95
|
sendMessage(input);
|
|
128
|
-
}, [handleCommand, sendMessage, app, viewMode,
|
|
96
|
+
}, [handleCommand, sendMessage, app, viewMode, addMsg]);
|
|
129
97
|
|
|
130
98
|
const allMessages = [...systemMessages, ...messages];
|
|
131
99
|
const activeSkills = app.skills.listActive().map(s => s.id);
|
|
@@ -137,6 +105,7 @@ export function TuiApp({ app }: Props) {
|
|
|
137
105
|
model={app.config.get().model}
|
|
138
106
|
cacheHitRate={cacheSnapshot.hitRate > 0 ? cacheSnapshot.hitRate : null}
|
|
139
107
|
cacheSavingsCNY={cacheSnapshot.estimatedSavingsCNY}
|
|
108
|
+
cacheTtl={resolvedTtl ?? configTtl ?? null}
|
|
140
109
|
/>
|
|
141
110
|
<ChatList
|
|
142
111
|
messages={allMessages}
|
|
@@ -164,6 +133,12 @@ export function TuiApp({ app }: Props) {
|
|
|
164
133
|
onAddNew={() => setViewMode('init_wizard')}
|
|
165
134
|
/>
|
|
166
135
|
) : viewMode === 'init_wizard' ? (
|
|
136
|
+
// v3.0.1: InitWizard is now fully self-contained — it owns its own
|
|
137
|
+
// input handling via useInput + ink-text-input. No external UserInput
|
|
138
|
+
// is rendered here, and app.tsx no longer has the text-entry
|
|
139
|
+
// viewModes (init_key / init_url / init_custom_model) because they
|
|
140
|
+
// were the broken input-handling path that left users stuck on a
|
|
141
|
+
// frozen "输入 API Key" screen with no way to type.
|
|
167
142
|
<InitWizard
|
|
168
143
|
onComplete={(provider, model, apiKey, baseUrl) => {
|
|
169
144
|
app.config.save({ provider, model, apiKey, baseUrl });
|
|
@@ -175,22 +150,6 @@ export function TuiApp({ app }: Props) {
|
|
|
175
150
|
/>
|
|
176
151
|
) : (
|
|
177
152
|
<Box flexDirection="column">
|
|
178
|
-
{viewMode === 'init_key' && (
|
|
179
|
-
<Box paddingLeft={1} marginBottom={0}>
|
|
180
|
-
<Text color="#06B6D4" bold>输入 API Key: </Text>
|
|
181
|
-
<Text dimColor>({initProvider})</Text>
|
|
182
|
-
</Box>
|
|
183
|
-
)}
|
|
184
|
-
{viewMode === 'init_url' && (
|
|
185
|
-
<Box paddingLeft={1} marginBottom={0}>
|
|
186
|
-
<Text color="#06B6D4" bold>输入中转站 URL: </Text>
|
|
187
|
-
</Box>
|
|
188
|
-
)}
|
|
189
|
-
{viewMode === 'init_custom_model' && (
|
|
190
|
-
<Box paddingLeft={1} marginBottom={0}>
|
|
191
|
-
<Text color="#06B6D4" bold>输入模型名称: </Text>
|
|
192
|
-
</Box>
|
|
193
|
-
)}
|
|
194
153
|
<UserInput onSubmit={onSubmit} onCancel={cancel} disabled={false} />
|
|
195
154
|
</Box>
|
|
196
155
|
)}
|
|
@@ -13,6 +13,13 @@ interface Props {
|
|
|
13
13
|
*/
|
|
14
14
|
cacheHitRate?: number | null;
|
|
15
15
|
cacheSavingsCNY?: number;
|
|
16
|
+
/**
|
|
17
|
+
* v3.0.3: cache TTL marker. '5m' (default for short sessions) or '1h'
|
|
18
|
+
* (auto-decided for long sessions, or pinned by the user). 'auto' is
|
|
19
|
+
* the user-pinned setting and shows up as ⏱ auto. null = not yet
|
|
20
|
+
* decided (waiting for first round).
|
|
21
|
+
*/
|
|
22
|
+
cacheTtl?: '5m' | '1h' | 'auto' | null;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -27,7 +34,7 @@ function hitRateColor(rate: number): string {
|
|
|
27
34
|
return '#6B7280';
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY }: Props) {
|
|
37
|
+
export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY, cacheTtl }: Props) {
|
|
31
38
|
const showCache = typeof cacheHitRate === 'number' && cacheHitRate > 0;
|
|
32
39
|
return (
|
|
33
40
|
<Box flexDirection="column" marginBottom={0}>
|
|
@@ -35,7 +42,7 @@ export const Header = React.memo(function Header({ provider, model, cacheHitRate
|
|
|
35
42
|
<Box>
|
|
36
43
|
<Text color="#06B6D4" bold> ⚡ </Text>
|
|
37
44
|
<Text color="#22D3EE" bold>THATGFSJ CODE</Text>
|
|
38
|
-
<Text dimColor> v3.0.
|
|
45
|
+
<Text dimColor> v3.0.3</Text>
|
|
39
46
|
</Box>
|
|
40
47
|
<Box>
|
|
41
48
|
{showCache && (
|
|
@@ -45,6 +52,9 @@ export const Header = React.memo(function Header({ provider, model, cacheHitRate
|
|
|
45
52
|
<Text dimColor> · </Text>
|
|
46
53
|
</>
|
|
47
54
|
)}
|
|
55
|
+
{cacheTtl && (
|
|
56
|
+
<Text color="#A78BFA" bold> ⏱ {cacheTtl} </Text>
|
|
57
|
+
)}
|
|
48
58
|
<Text color="#06B6D4" bold> {provider} </Text>
|
|
49
59
|
<Text dimColor>/</Text>
|
|
50
60
|
<Text color="#22D3EE"> {model} </Text>
|