mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: 修复 anthropic 煞笔的四个 bug (#352)
* fix: 移除文件编辑前必须先读取的限制 移除 FileEditTool 和 FileWriteTool 中的 "read before edit" 校验, 允许直接编辑未读取过的文件。保留文件修改过期检测。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 更新 teach-me 自动写 note 笔记的功能 * fix: 修复 DeepSeek V4 reasoning_content 回传导致的 400 错误 - 扩大模型名称检测范围,匹配所有 deepseek 模型(V4、R1 等) - 始终保留 thinking blocks 为 reasoning_content 回传给 API - 移除有 bug 的 turn boundary 剥离逻辑 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: Opus 4.6/4.7 默认推理 effort 从 medium 改为 high Pro 和 Max/Team 订阅者的 Opus 默认 effort 之前被降级为 medium, 导致用户感知模型「变笨」。恢复为 high。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 移除 thinkingClearLatched sticky-on 机制 空闲超过 1 小时后 thinkingClearLatched 会被触发且永不重置, 导致每轮 API 调用都清除 thinking 历史。完整移除该 latch 机制, clearAllThinking 硬编码为 false。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 移除 numeric_length_anchors 系统指令 删除「工具调用间文字 ≤25 词、最终回复 ≤100 词」的硬性限制。 ablation 测试显示该约束使整体智能下降 3%。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 修复测试中 reasoning_content 类型断言 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,8 @@ All teach-me data is stored under `.claude/skills/teach-me/records/`:
|
|||||||
.claude/skills/teach-me/records/
|
.claude/skills/teach-me/records/
|
||||||
├── learner-profile.md # Cross-topic notes (created on first session)
|
├── learner-profile.md # Cross-topic notes (created on first session)
|
||||||
└── {topic-slug}/
|
└── {topic-slug}/
|
||||||
└── session.md # Learning state: concepts, status, notes
|
├── session.md # Learning state: concepts, status, notes
|
||||||
|
└── {topic-slug}-notes.md # Learner-facing summary notes (generated at session end)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Slug**: Topic in kebab-case, 2-5 words. Example: "Python decorators" → `python-decorators`
|
**Slug**: Topic in kebab-case, 2-5 words. Example: "Python decorators" → `python-decorators`
|
||||||
@@ -275,7 +276,8 @@ Update `session.md` after each round:
|
|||||||
When all concepts mastered or user ends session:
|
When all concepts mastered or user ends session:
|
||||||
|
|
||||||
1. Update `session.md` with final state.
|
1. Update `session.md` with final state.
|
||||||
2. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
|
2. **Generate learner-facing notes** — write `{topic-slug}-notes.md` in the topic directory. This is a standalone reference document the learner can review later. See "Notes Generation" below for format.
|
||||||
|
3. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# Learner Profile
|
# Learner Profile
|
||||||
@@ -293,7 +295,48 @@ Updated: {timestamp}
|
|||||||
- Python decorators (8/10 concepts, 2025-01-15)
|
- Python decorators (8/10 concepts, 2025-01-15)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Give a brief text summary of what was covered, key insights, and areas for further study.
|
4. Give a brief text summary of what was covered, key insights, and areas for further study.
|
||||||
|
|
||||||
|
## Notes Generation
|
||||||
|
|
||||||
|
At session end, generate a learner-facing notes file at `{topic-slug}/{topic-slug}-notes.md`. This file is **written for the learner to review later**, not for the tutor. It should be self-contained and organized as a quick-reference.
|
||||||
|
|
||||||
|
### Notes Structure
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# {Topic} 核心笔记
|
||||||
|
|
||||||
|
## 1. {Section Name}
|
||||||
|
{Key concept, mechanism, or principle}
|
||||||
|
* **One-line summary**: {what it does / why it matters}
|
||||||
|
* **Detail**: {brief explanation, 2-4 sentences max}
|
||||||
|
* **Example** (if applicable): {code snippet, command, or concrete scenario}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. {Section Name}
|
||||||
|
...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## n. 实战参数 / Cheat Sheet (if applicable)
|
||||||
|
{Practical commands, config, or quick-reference table}
|
||||||
|
|
||||||
|
| Parameter / Concept | What it does | Tuning tip |
|
||||||
|
|---------------------|-------------|------------|
|
||||||
|
| ... | ... | ... |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes Writing Rules
|
||||||
|
|
||||||
|
1. **Start with "what & why"** before "how". Each section should answer: what is this, why does it exist, what problem does it solve.
|
||||||
|
2. **Use analogies sparingly but effectively**. Only include an analogy if it clarifies a non-obvious mechanism (e.g., "PagedAttention is like OS virtual memory paging").
|
||||||
|
3. **Include trade-offs**. Every optimization or design choice has a cost. Always state it (e.g., "TP improves throughput but increases communication latency").
|
||||||
|
4. **Code / command examples should be minimal**. Under 10 lines, self-contained, with comments explaining the key flags.
|
||||||
|
5. **Organize by concept dependency**, not by chronological teaching order. Foundation concepts first, advanced ones last.
|
||||||
|
6. **No quiz questions, no misconceptions, no tutor-side notes**. This is a clean reference document.
|
||||||
|
7. **Language matches the session**. If the session was in Chinese, notes are in Chinese (technical terms can stay in English).
|
||||||
|
8. **Keep it under 150 lines**. If it gets too long, the learner won't review it. Be ruthless about cutting fluff.
|
||||||
|
|
||||||
## Resuming Sessions
|
## Resuming Sessions
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('strips thinking blocks', () => {
|
test('preserves thinking blocks as reasoning_content', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[
|
||||||
makeAssistantMsg([
|
makeAssistantMsg([
|
||||||
@@ -131,7 +131,7 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
],
|
],
|
||||||
[] as any,
|
[] as any,
|
||||||
)
|
)
|
||||||
expect(result).toEqual([{ role: 'assistant', content: 'visible response' }])
|
expect(result).toEqual([{ role: 'assistant', content: 'visible response', reasoning_content: 'internal thoughts...' }] as any)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('handles full conversation with tools', () => {
|
test('handles full conversation with tools', () => {
|
||||||
@@ -299,7 +299,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
expect(assistant.reasoning_content).toBe('Let me reason about this...')
|
expect(assistant.reasoning_content).toBe('Let me reason about this...')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('drops thinking block when enableThinking is false (default)', () => {
|
test('preserves thinking block as reasoning_content even without enableThinking', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[
|
||||||
makeAssistantMsg([
|
makeAssistantMsg([
|
||||||
@@ -311,7 +311,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
)
|
)
|
||||||
const assistant = result[0] as any
|
const assistant = result[0] as any
|
||||||
expect(assistant.content).toBe('visible response')
|
expect(assistant.content).toBe('visible response')
|
||||||
expect(assistant.reasoning_content).toBeUndefined()
|
expect(assistant.reasoning_content).toBe('internal thoughts...')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('preserves reasoning_content with tool_calls in same turn', () => {
|
test('preserves reasoning_content with tool_calls in same turn', () => {
|
||||||
@@ -352,7 +352,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
expect(assistant.tool_calls[0].function.name).toBe('get_weather')
|
expect(assistant.tool_calls[0].function.name).toBe('get_weather')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('strips reasoning_content from previous turns', () => {
|
test('always preserves reasoning_content from all turns', () => {
|
||||||
const result = anthropicMessagesToOpenAI(
|
const result = anthropicMessagesToOpenAI(
|
||||||
[
|
[
|
||||||
// Turn 1: user → assistant (with thinking)
|
// Turn 1: user → assistant (with thinking)
|
||||||
@@ -361,7 +361,8 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
{ type: 'thinking' as const, thinking: 'Turn 1 reasoning...' },
|
{ type: 'thinking' as const, thinking: 'Turn 1 reasoning...' },
|
||||||
{ type: 'text', text: 'Turn 1 answer' },
|
{ type: 'text', text: 'Turn 1 answer' },
|
||||||
]),
|
]),
|
||||||
// Turn 2: new user message → previous reasoning should be stripped
|
// Turn 2: new user message → reasoning should still be preserved
|
||||||
|
// (DeepSeek requires reasoning_content to be passed back when tool calls are involved)
|
||||||
makeUserMsg('question 2'),
|
makeUserMsg('question 2'),
|
||||||
makeAssistantMsg([
|
makeAssistantMsg([
|
||||||
{ type: 'thinking' as const, thinking: 'Turn 2 reasoning...' },
|
{ type: 'thinking' as const, thinking: 'Turn 2 reasoning...' },
|
||||||
@@ -373,10 +374,9 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const assistants = result.filter(m => m.role === 'assistant')
|
const assistants = result.filter(m => m.role === 'assistant')
|
||||||
// Turn 1 assistant: reasoning should be stripped (previous turn)
|
// Both turns preserve reasoning_content (DeepSeek API requires it for tool calls)
|
||||||
expect((assistants[0] as any).reasoning_content).toBeUndefined()
|
expect((assistants[0] as any).reasoning_content).toBe('Turn 1 reasoning...')
|
||||||
expect((assistants[0] as any).content).toBe('Turn 1 answer')
|
expect((assistants[0] as any).content).toBe('Turn 1 answer')
|
||||||
// Turn 2 assistant: reasoning should be preserved (current turn)
|
|
||||||
expect((assistants[1] as any).reasoning_content).toBe('Turn 2 reasoning...')
|
expect((assistants[1] as any).reasoning_content).toBe('Turn 2 reasoning...')
|
||||||
expect((assistants[1] as any).content).toBe('Turn 2 answer')
|
expect((assistants[1] as any).content).toBe('Turn 2 answer')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ export interface ConvertMessagesOptions {
|
|||||||
* - system prompt → role: "system" message prepended
|
* - system prompt → role: "system" message prepended
|
||||||
* - tool_use blocks → tool_calls[] on assistant message
|
* - tool_use blocks → tool_calls[] on assistant message
|
||||||
* - tool_result blocks → role: "tool" messages
|
* - tool_result blocks → role: "tool" messages
|
||||||
* - thinking blocks → silently dropped (or preserved as reasoning_content when enableThinking=true)
|
* - thinking blocks → preserved as reasoning_content (DeepSeek requires passing it back)
|
||||||
* - cache_control → stripped
|
* - cache_control → stripped
|
||||||
*/
|
*/
|
||||||
export function anthropicMessagesToOpenAI(
|
export function anthropicMessagesToOpenAI(
|
||||||
messages: (UserMessage | AssistantMessage)[],
|
messages: (UserMessage | AssistantMessage)[],
|
||||||
systemPrompt: SystemPrompt,
|
systemPrompt: SystemPrompt,
|
||||||
options?: ConvertMessagesOptions,
|
// options retained for API compatibility; thinking blocks are now always preserved
|
||||||
|
_options?: ConvertMessagesOptions,
|
||||||
): ChatCompletionMessageParam[] {
|
): ChatCompletionMessageParam[] {
|
||||||
const result: ChatCompletionMessageParam[] = []
|
const result: ChatCompletionMessageParam[] = []
|
||||||
const enableThinking = options?.enableThinking ?? false
|
|
||||||
|
|
||||||
// Prepend system prompt as system message
|
// Prepend system prompt as system message
|
||||||
const systemText = systemPromptToText(systemPrompt)
|
const systemText = systemPromptToText(systemPrompt)
|
||||||
@@ -46,53 +46,13 @@ export function anthropicMessagesToOpenAI(
|
|||||||
} satisfies ChatCompletionSystemMessageParam)
|
} satisfies ChatCompletionSystemMessageParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When thinking mode is on, detect turn boundaries so that reasoning_content
|
for (const msg of messages) {
|
||||||
// from *previous* user turns is stripped (saves bandwidth; DeepSeek ignores it).
|
|
||||||
// A "new turn" starts when a user text message appears after at least one assistant response.
|
|
||||||
const turnBoundaries = new Set<number>()
|
|
||||||
if (enableThinking) {
|
|
||||||
let hasSeenAssistant = false
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
|
||||||
const msg = messages[i]
|
|
||||||
if (msg.type === 'assistant') {
|
|
||||||
hasSeenAssistant = true
|
|
||||||
}
|
|
||||||
if (msg.type === 'user' && hasSeenAssistant) {
|
|
||||||
const content = msg.message.content
|
|
||||||
// A user message starts a new turn if it contains any non-tool_result content
|
|
||||||
// (text, image, or other media). Tool results alone do NOT start a new turn
|
|
||||||
// because they are continuations of the previous assistant tool call.
|
|
||||||
const startsNewUserTurn =
|
|
||||||
typeof content === 'string'
|
|
||||||
? content.length > 0
|
|
||||||
: Array.isArray(content) &&
|
|
||||||
content.some(
|
|
||||||
(b: any) =>
|
|
||||||
typeof b === 'string' ||
|
|
||||||
(b &&
|
|
||||||
typeof b === 'object' &&
|
|
||||||
'type' in b &&
|
|
||||||
b.type !== 'tool_result'),
|
|
||||||
)
|
|
||||||
if (startsNewUserTurn) {
|
|
||||||
turnBoundaries.add(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
|
||||||
const msg = messages[i]
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'user':
|
case 'user':
|
||||||
result.push(...convertInternalUserMessage(msg))
|
result.push(...convertInternalUserMessage(msg))
|
||||||
break
|
break
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
// Preserve reasoning_content unless we're before a turn boundary
|
result.push(...convertInternalAssistantMessage(msg))
|
||||||
// (i.e., from a previous user Q&A round)
|
|
||||||
const preserveReasoning =
|
|
||||||
enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
|
|
||||||
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -107,17 +67,6 @@ function systemPromptToText(systemPrompt: SystemPrompt): string {
|
|||||||
return systemPrompt.filter(Boolean).join('\n\n')
|
return systemPrompt.filter(Boolean).join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if index `i` falls before any turn boundary (i.e. it belongs to a previous turn).
|
|
||||||
* A message at index i is "before" a boundary if there exists a boundary j where i < j.
|
|
||||||
*/
|
|
||||||
function isBeforeAnyTurnBoundary(i: number, boundaries: Set<number>): boolean {
|
|
||||||
for (const b of boundaries) {
|
|
||||||
if (i < b) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertInternalUserMessage(
|
function convertInternalUserMessage(
|
||||||
msg: UserMessage,
|
msg: UserMessage,
|
||||||
): ChatCompletionMessageParam[] {
|
): ChatCompletionMessageParam[] {
|
||||||
@@ -213,7 +162,6 @@ function convertToolResult(
|
|||||||
|
|
||||||
function convertInternalAssistantMessage(
|
function convertInternalAssistantMessage(
|
||||||
msg: AssistantMessage,
|
msg: AssistantMessage,
|
||||||
preserveReasoning = false,
|
|
||||||
): ChatCompletionMessageParam[] {
|
): ChatCompletionMessageParam[] {
|
||||||
const content = msg.message.content
|
const content = msg.message.content
|
||||||
|
|
||||||
@@ -257,8 +205,10 @@ function convertInternalAssistantMessage(
|
|||||||
typeof tu.input === 'string' ? tu.input : JSON.stringify(tu.input),
|
typeof tu.input === 'string' ? tu.input : JSON.stringify(tu.input),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (block.type === 'thinking' && preserveReasoning) {
|
} else if (block.type === 'thinking') {
|
||||||
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
|
// DeepSeek thinking mode: always preserve reasoning_content.
|
||||||
|
// DeepSeek requires reasoning_content to be passed back in subsequent requests,
|
||||||
|
// especially when tool calls are involved (returns 400 if missing).
|
||||||
const thinkingText = (block as unknown as Record<string, unknown>)
|
const thinkingText = (block as unknown as Record<string, unknown>)
|
||||||
.thinking
|
.thinking
|
||||||
if (typeof thinkingText === 'string' && thinkingText) {
|
if (typeof thinkingText === 'string' && thinkingText) {
|
||||||
|
|||||||
@@ -273,18 +273,6 @@ export const FileEditTool = buildTool({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
|
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
|
||||||
if (!readTimestamp || readTimestamp.isPartialView) {
|
|
||||||
return {
|
|
||||||
result: false,
|
|
||||||
behavior: 'ask',
|
|
||||||
message:
|
|
||||||
'File has not been read yet. Read it first before writing to it.',
|
|
||||||
meta: {
|
|
||||||
isFilePathAbsolute: String(isAbsolute(file_path)),
|
|
||||||
},
|
|
||||||
errorCode: 6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists and get its last modified time
|
// Check if file exists and get its last modified time
|
||||||
if (readTimestamp) {
|
if (readTimestamp) {
|
||||||
|
|||||||
@@ -186,14 +186,6 @@ export function renderToolUseErrorMessage(
|
|||||||
extractTag(result, 'tool_use_error')
|
extractTag(result, 'tool_use_error')
|
||||||
) {
|
) {
|
||||||
const errorMessage = extractTag(result, 'tool_use_error')
|
const errorMessage = extractTag(result, 'tool_use_error')
|
||||||
// Show a less scary message for intended behavior
|
|
||||||
if (errorMessage?.includes('File has not been read yet')) {
|
|
||||||
return (
|
|
||||||
<MessageResponse>
|
|
||||||
<Text dimColor>File must be read first</Text>
|
|
||||||
</MessageResponse>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
|
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
|
||||||
return (
|
return (
|
||||||
<MessageResponse>
|
<MessageResponse>
|
||||||
|
|||||||
@@ -196,18 +196,10 @@ export const FileWriteTool = buildTool({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
|
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
|
||||||
if (!readTimestamp || readTimestamp.isPartialView) {
|
|
||||||
return {
|
|
||||||
result: false,
|
|
||||||
message:
|
|
||||||
'File has not been read yet. Read it first before writing to it.',
|
|
||||||
errorCode: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reuse mtime from the stat above — avoids a redundant statSync via
|
// Reuse mtime from the stat above — avoids a redundant statSync via
|
||||||
// getFileModificationTime. The readTimestamp guard above ensures this
|
// getFileModificationTime.
|
||||||
// block is always reached when the file exists.
|
if (readTimestamp) {
|
||||||
const lastWriteTime = Math.floor(fileMtimeMs)
|
const lastWriteTime = Math.floor(fileMtimeMs)
|
||||||
if (lastWriteTime > readTimestamp.timestamp) {
|
if (lastWriteTime > readTimestamp.timestamp) {
|
||||||
return {
|
return {
|
||||||
@@ -217,6 +209,7 @@ export const FileWriteTool = buildTool({
|
|||||||
errorCode: 3,
|
errorCode: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { result: true }
|
return { result: true }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -235,11 +235,6 @@ type State = {
|
|||||||
// microcompact is first enabled, keep sending the header so mid-session
|
// microcompact is first enabled, keep sending the header so mid-session
|
||||||
// GrowthBook/settings toggles don't bust the prompt cache.
|
// GrowthBook/settings toggles don't bust the prompt cache.
|
||||||
cacheEditingHeaderLatched: boolean | null
|
cacheEditingHeaderLatched: boolean | null
|
||||||
// Sticky-on latch for clearing thinking from prior tool loops. Triggered
|
|
||||||
// when >1h since last API call (confirmed cache miss — no cache-hit
|
|
||||||
// benefit to keeping thinking). Once latched, stays on so the newly-warmed
|
|
||||||
// thinking-cleared cache isn't busted by flipping back to keep:'all'.
|
|
||||||
thinkingClearLatched: boolean | null
|
|
||||||
// Current prompt ID (UUID) correlating a user prompt with subsequent OTel events
|
// Current prompt ID (UUID) correlating a user prompt with subsequent OTel events
|
||||||
promptId: string | null
|
promptId: string | null
|
||||||
// Last API requestId for the main conversation chain (not subagents).
|
// Last API requestId for the main conversation chain (not subagents).
|
||||||
@@ -414,7 +409,6 @@ function getInitialState(): State {
|
|||||||
afkModeHeaderLatched: null,
|
afkModeHeaderLatched: null,
|
||||||
fastModeHeaderLatched: null,
|
fastModeHeaderLatched: null,
|
||||||
cacheEditingHeaderLatched: null,
|
cacheEditingHeaderLatched: null,
|
||||||
thinkingClearLatched: null,
|
|
||||||
// Current prompt ID
|
// Current prompt ID
|
||||||
promptId: null,
|
promptId: null,
|
||||||
lastMainRequestId: undefined,
|
lastMainRequestId: undefined,
|
||||||
@@ -1729,14 +1723,6 @@ export function setCacheEditingHeaderLatched(v: boolean): void {
|
|||||||
STATE.cacheEditingHeaderLatched = v
|
STATE.cacheEditingHeaderLatched = v
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThinkingClearLatched(): boolean | null {
|
|
||||||
return STATE.thinkingClearLatched
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setThinkingClearLatched(v: boolean): void {
|
|
||||||
STATE.thinkingClearLatched = v
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset beta header latches to null. Called on /clear and /compact so a
|
* Reset beta header latches to null. Called on /clear and /compact so a
|
||||||
* fresh conversation gets fresh header evaluation.
|
* fresh conversation gets fresh header evaluation.
|
||||||
@@ -1745,7 +1731,6 @@ export function clearBetaHeaderLatches(): void {
|
|||||||
STATE.afkModeHeaderLatched = null
|
STATE.afkModeHeaderLatched = null
|
||||||
STATE.fastModeHeaderLatched = null
|
STATE.fastModeHeaderLatched = null
|
||||||
STATE.cacheEditingHeaderLatched = null
|
STATE.cacheEditingHeaderLatched = null
|
||||||
STATE.thinkingClearLatched = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPromptId(): string | null {
|
export function getPromptId(): string | null {
|
||||||
|
|||||||
@@ -614,17 +614,6 @@ ${CYBER_RISK_INSTRUCTION}`,
|
|||||||
'summarize_tool_results',
|
'summarize_tool_results',
|
||||||
() => SUMMARIZE_TOOL_RESULTS_SECTION,
|
() => SUMMARIZE_TOOL_RESULTS_SECTION,
|
||||||
),
|
),
|
||||||
// Numeric length anchors — research shows ~1.2% output token reduction vs
|
|
||||||
// qualitative "be concise". Ant-only to measure quality impact first.
|
|
||||||
...(process.env.USER_TYPE === 'ant'
|
|
||||||
? [
|
|
||||||
systemPromptSection(
|
|
||||||
'numeric_length_anchors',
|
|
||||||
() =>
|
|
||||||
'Length limits: keep text between tool calls to \u226425 words. Keep final responses to \u2264100 words unless the task requires more detail.',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(feature('TOKEN_BUDGET')
|
...(feature('TOKEN_BUDGET')
|
||||||
? [
|
? [
|
||||||
// Cached unconditionally — the "When the user specifies..." phrasing
|
// Cached unconditionally — the "When the user specifies..." phrasing
|
||||||
|
|||||||
@@ -124,14 +124,12 @@ import {
|
|||||||
getPromptCache1hAllowlist,
|
getPromptCache1hAllowlist,
|
||||||
getPromptCache1hEligible,
|
getPromptCache1hEligible,
|
||||||
getSessionId,
|
getSessionId,
|
||||||
getThinkingClearLatched,
|
|
||||||
setAfkModeHeaderLatched,
|
setAfkModeHeaderLatched,
|
||||||
setCacheEditingHeaderLatched,
|
setCacheEditingHeaderLatched,
|
||||||
setFastModeHeaderLatched,
|
setFastModeHeaderLatched,
|
||||||
setLastMainRequestId,
|
setLastMainRequestId,
|
||||||
setPromptCache1hAllowlist,
|
setPromptCache1hAllowlist,
|
||||||
setPromptCache1hEligible,
|
setPromptCache1hEligible,
|
||||||
setThinkingClearLatched,
|
|
||||||
} from 'src/bootstrap/state.js'
|
} from 'src/bootstrap/state.js'
|
||||||
import {
|
import {
|
||||||
AFK_MODE_BETA_HEADER,
|
AFK_MODE_BETA_HEADER,
|
||||||
@@ -1492,20 +1490,6 @@ async function* queryModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only latch from agentic queries so a classifier call doesn't flip the
|
|
||||||
// main thread's context_management mid-turn.
|
|
||||||
let thinkingClearLatched = getThinkingClearLatched() === true
|
|
||||||
if (!thinkingClearLatched && isAgenticQuery) {
|
|
||||||
const lastCompletion = getLastApiCompletionTimestamp()
|
|
||||||
if (
|
|
||||||
lastCompletion !== null &&
|
|
||||||
Date.now() - lastCompletion > CACHE_TTL_1HOUR_MS
|
|
||||||
) {
|
|
||||||
thinkingClearLatched = true
|
|
||||||
setThinkingClearLatched(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const effort = resolveAppliedEffort(options.model, options.effortValue)
|
const effort = resolveAppliedEffort(options.model, options.effortValue)
|
||||||
|
|
||||||
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
|
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
|
||||||
@@ -1684,7 +1668,7 @@ async function* queryModel(
|
|||||||
const contextManagement = getAPIContextManagement({
|
const contextManagement = getAPIContextManagement({
|
||||||
hasThinking,
|
hasThinking,
|
||||||
isRedactThinkingActive: betasParams.includes(REDACT_THINKING_BETA_HEADER),
|
isRedactThinkingActive: betasParams.includes(REDACT_THINKING_BETA_HEADER),
|
||||||
clearAllThinking: thinkingClearLatched,
|
clearAllThinking: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const enablePromptCaching =
|
const enablePromptCaching =
|
||||||
|
|||||||
@@ -100,16 +100,28 @@ describe('isOpenAIThinkingEnabled', () => {
|
|||||||
expect(isOpenAIThinkingEnabled('TokenService/deepseek-v3.2')).toBe(true)
|
expect(isOpenAIThinkingEnabled('TokenService/deepseek-v3.2')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false when model name is "deepseek-chat"', () => {
|
test('returns true when model name is "deepseek-chat"', () => {
|
||||||
expect(isOpenAIThinkingEnabled('deepseek-chat')).toBe(false)
|
expect(isOpenAIThinkingEnabled('deepseek-chat')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false when model name is "deepseek-v3"', () => {
|
test('returns true when model name is "deepseek-v3"', () => {
|
||||||
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(false)
|
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false when model name contains "deepseek" but not "reasoner" or "v3.2"', () => {
|
test('returns true when model name is "deepseek-v4"', () => {
|
||||||
expect(isOpenAIThinkingEnabled('deepseek-coder')).toBe(false)
|
expect(isOpenAIThinkingEnabled('deepseek-v4')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns true when model name is "deepseek-v4-pro"', () => {
|
||||||
|
expect(isOpenAIThinkingEnabled('deepseek-v4-pro')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns true when model name is "deepseek-r1"', () => {
|
||||||
|
expect(isOpenAIThinkingEnabled('deepseek-r1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns true when model name contains "deepseek"', () => {
|
||||||
|
expect(isOpenAIThinkingEnabled('deepseek-coder')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns false when model name is "gpt-4o"', () => {
|
test('returns false when model name is "gpt-4o"', () => {
|
||||||
@@ -126,6 +138,7 @@ describe('isOpenAIThinkingEnabled', () => {
|
|||||||
process.env.OPENAI_ENABLE_THINKING = '1'
|
process.env.OPENAI_ENABLE_THINKING = '1'
|
||||||
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
|
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
|
||||||
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
|
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
|
||||||
|
expect(isOpenAIThinkingEnabled('qwen-3')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('OPENAI_ENABLE_THINKING=false disables thinking even for deepseek-reasoner', () => {
|
test('OPENAI_ENABLE_THINKING=false disables thinking even for deepseek-reasoner', () => {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export function isOpenAIThinkingEnabled(model: string): boolean {
|
|||||||
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
|
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
|
||||||
// Explicit enable
|
// Explicit enable
|
||||||
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
|
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
|
||||||
// Auto-detect from model name (deepseek-reasoner and DeepSeek-V3.2 support thinking mode)
|
// Auto-detect from model name (all DeepSeek models support thinking mode)
|
||||||
const modelLower = model.toLowerCase()
|
const modelLower = model.toLowerCase()
|
||||||
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2')
|
return modelLower.includes('deepseek')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ export type getFastModeHeaderLatched = any;
|
|||||||
export type getLastApiCompletionTimestamp = any;
|
export type getLastApiCompletionTimestamp = any;
|
||||||
export type getPromptCache1hAllowlist = any;
|
export type getPromptCache1hAllowlist = any;
|
||||||
export type getPromptCache1hEligible = any;
|
export type getPromptCache1hEligible = any;
|
||||||
export type getThinkingClearLatched = any;
|
|
||||||
export type setAfkModeHeaderLatched = any;
|
export type setAfkModeHeaderLatched = any;
|
||||||
export type setCacheEditingHeaderLatched = any;
|
export type setCacheEditingHeaderLatched = any;
|
||||||
export type setFastModeHeaderLatched = any;
|
export type setFastModeHeaderLatched = any;
|
||||||
export type setLastMainRequestId = any;
|
export type setLastMainRequestId = any;
|
||||||
export type setPromptCache1hAllowlist = any;
|
export type setPromptCache1hAllowlist = any;
|
||||||
export type setPromptCache1hEligible = any;
|
export type setPromptCache1hEligible = any;
|
||||||
export type setThinkingClearLatched = any;
|
|
||||||
export type addToTotalDurationState = any;
|
export type addToTotalDurationState = any;
|
||||||
export type consumePostCompaction = any;
|
export type consumePostCompaction = any;
|
||||||
export type getIsNonInteractiveSession = any;
|
export type getIsNonInteractiveSession = any;
|
||||||
|
|||||||
@@ -348,13 +348,13 @@ export function getDefaultEffortForModel(
|
|||||||
model.toLowerCase().includes('opus-4-6')
|
model.toLowerCase().includes('opus-4-6')
|
||||||
) {
|
) {
|
||||||
if (isProSubscriber()) {
|
if (isProSubscriber()) {
|
||||||
return 'medium'
|
return 'high'
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
getOpusDefaultEffortConfig().enabled &&
|
getOpusDefaultEffortConfig().enabled &&
|
||||||
(isMaxSubscriber() || isTeamSubscriber())
|
(isMaxSubscriber() || isTeamSubscriber())
|
||||||
) {
|
) {
|
||||||
return 'medium'
|
return 'high'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user