mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
6 Commits
v1.10.5
...
feature/la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fcdcd6018 | ||
|
|
4cbef9667d | ||
|
|
c6338917e5 | ||
|
|
bcbb8a6e93 | ||
|
|
3fb48ec106 | ||
|
|
36bf4f260f |
@@ -55,6 +55,8 @@ ccb update # 更新到最新版本
|
||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
||||
```
|
||||
|
||||
> **安装/更新失败?** 先 `npm rm -g claude-code-best` 清理旧版本,再 `npm i -g claude-code-best@latest`。仍失败则指定版本号:`npm i -g claude-code-best@<版本号>`
|
||||
|
||||
## ⚡ 快速开始(源码版)
|
||||
|
||||
### ⚙️ 环境要求
|
||||
|
||||
@@ -106,6 +106,84 @@ describe("findActualString", () => {
|
||||
const result = findActualString("hello", "");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
// ── Tab/space normalization (Bug #2 reproduction) ──
|
||||
|
||||
test("finds match when search uses spaces but file uses tabs", () => {
|
||||
// File content uses Tab indentation
|
||||
const fileContent = "\tif (x) {\n\t\treturn 1;\n\t}";
|
||||
// User copies from Read output which renders tabs as spaces
|
||||
const searchWithSpaces = " if (x) {\n return 1;\n }";
|
||||
const result = findActualString(fileContent, searchWithSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toBe(fileContent);
|
||||
});
|
||||
|
||||
test("finds match when search mixes tabs and spaces inconsistently", () => {
|
||||
const fileContent = "\tconst x = 1; // comment";
|
||||
const searchMixed = " const x = 1; // comment";
|
||||
const result = findActualString(fileContent, searchMixed);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
test("finds match for single-line tab-to-space mismatch", () => {
|
||||
const fileContent = "\t\torder_price = NormalizeDouble(ask, digits);";
|
||||
const searchSpaces = " order_price = NormalizeDouble(ask, digits);";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── CJK / UTF-8 characters (Bug #1 reproduction) ──
|
||||
|
||||
test("finds match with CJK characters in content", () => {
|
||||
const fileContent = "input int x = 620; // 止盈点数(点) — 32个pip=320点";
|
||||
const result = findActualString(fileContent, fileContent);
|
||||
expect(result).toBe(fileContent);
|
||||
});
|
||||
|
||||
test("finds match with CJK characters when tab/space differs", () => {
|
||||
const fileContent = "\t// 向上突破 → Sell Limit (逆方向做空)";
|
||||
const searchSpaces = " // 向上突破 → Sell Limit (逆方向做空)";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toBe(fileContent);
|
||||
});
|
||||
|
||||
// ── Multiline with tabs + CJK (combined Bug #1 + #2) ──
|
||||
|
||||
test("finds multiline match with tabs and CJK characters", () => {
|
||||
const fileContent = "\tif(effective_dir == BREAKOUT_UP)\n\t\t{\n\t\t\t// 向上突破\n\t\t}";
|
||||
const searchSpaces = " if(effective_dir == BREAKOUT_UP)\n {\n // 向上突破\n }";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toBe(fileContent);
|
||||
});
|
||||
|
||||
// ── Returned string must be a valid substring of fileContent ──
|
||||
|
||||
test("returned string from tab match is a real substring of fileContent", () => {
|
||||
const fileContent = "prefix\n\t\tindented code\nsuffix";
|
||||
const searchSpaces = "prefix\n indented code\nsuffix";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(fileContent.includes(result!)).toBe(true);
|
||||
});
|
||||
|
||||
test("returned string from partial tab match is a real substring", () => {
|
||||
const fileContent = "line1\n\tif (x) {\n\t\tdoStuff();\n\t}\nline5";
|
||||
const searchSpaces = " if (x) {\n doStuff();\n }";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(fileContent.includes(result!)).toBe(true);
|
||||
});
|
||||
|
||||
test("tab match with mixed indentation levels", () => {
|
||||
const fileContent = "class Foo {\n\t\tmethod1() {\n\t\t\treturn 42;\n\t\t}\n}";
|
||||
const searchSpaces = "class Foo {\n method1() {\n return 42;\n }\n}";
|
||||
const result = findActualString(fileContent, searchSpaces);
|
||||
expect(result).not.toBeNull();
|
||||
expect(fileContent.includes(result!)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── preserveQuoteStyle ─────────────────────────────────────────────────
|
||||
|
||||
@@ -63,9 +63,26 @@ export function stripTrailingWhitespace(str: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes whitespace for fuzzy matching by converting tabs to spaces
|
||||
* and collapsing leading whitespace on each line to a canonical form.
|
||||
* This handles the case where Read tool output renders tabs as spaces,
|
||||
* so users copy spaces from the output but the file actually has tabs.
|
||||
*/
|
||||
function normalizeWhitespace(str: string): string {
|
||||
return str.replace(/\t/g, ' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the actual string in the file content that matches the search string,
|
||||
* accounting for quote normalization
|
||||
* accounting for quote normalization and tab/space differences.
|
||||
*
|
||||
* Matching cascade:
|
||||
* 1. Exact match
|
||||
* 2. Quote normalization (curly → straight quotes)
|
||||
* 3. Tab/space normalization (tabs ↔ spaces in leading whitespace)
|
||||
* 4. Quote + tab/space normalization combined
|
||||
*
|
||||
* @param fileContent The file content to search in
|
||||
* @param searchString The string to search for
|
||||
* @returns The actual string found in the file, or null if not found
|
||||
@@ -89,9 +106,92 @@ export function findActualString(
|
||||
return fileContent.substring(searchIndex, searchIndex + searchString.length)
|
||||
}
|
||||
|
||||
// Try with tab/space normalization — handles the case where Read output
|
||||
// renders tabs as spaces and the user copies the rendered version
|
||||
const wsNormalizedFile = normalizeWhitespace(fileContent)
|
||||
const wsNormalizedSearch = normalizeWhitespace(searchString)
|
||||
|
||||
const wsSearchIndex = wsNormalizedFile.indexOf(wsNormalizedSearch)
|
||||
if (wsSearchIndex !== -1) {
|
||||
// Map the match position back to the original file content.
|
||||
// We need to find the corresponding range in the original string.
|
||||
return mapNormalizedMatchBackToFile(fileContent, wsNormalizedFile, wsSearchIndex, wsNormalizedSearch.length)
|
||||
}
|
||||
|
||||
// Try combined: quote normalization + tab/space normalization
|
||||
const combinedFile = normalizeWhitespace(normalizedFile)
|
||||
const combinedSearch = normalizeWhitespace(normalizedSearch)
|
||||
|
||||
const combinedIndex = combinedFile.indexOf(combinedSearch)
|
||||
if (combinedIndex !== -1) {
|
||||
return mapNormalizedMatchBackToFile(fileContent, combinedFile, combinedIndex, combinedSearch.length)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a match found in a normalized version of fileContent, map the match
|
||||
* position back to the original fileContent and extract the corresponding
|
||||
* substring.
|
||||
*
|
||||
* Strategy: walk through both strings character by character, building a
|
||||
* mapping from normalized offset to original offset. When a tab is expanded
|
||||
* to 4 spaces in the normalized version, the normalized offset advances by 4
|
||||
* while the original offset advances by 1.
|
||||
*/
|
||||
function mapNormalizedMatchBackToFile(
|
||||
fileContent: string,
|
||||
normalizedFile: string,
|
||||
normalizedStart: number,
|
||||
normalizedLength: number,
|
||||
): string {
|
||||
// Build a sparse mapping from normalized position → original position.
|
||||
// We only need to map the range [normalizedStart, normalizedStart + normalizedLength].
|
||||
let normPos = 0
|
||||
let origPos = 0
|
||||
let origStart = -1
|
||||
let origEnd = -1
|
||||
|
||||
while (origPos < fileContent.length && normPos <= normalizedStart + normalizedLength) {
|
||||
if (normPos === normalizedStart) {
|
||||
origStart = origPos
|
||||
}
|
||||
if (normPos === normalizedStart + normalizedLength) {
|
||||
origEnd = origPos
|
||||
break
|
||||
}
|
||||
|
||||
const origChar = fileContent[origPos]!
|
||||
if (origChar === '\t') {
|
||||
// Tab expands to 4 spaces in normalized version
|
||||
const nextNormPos = normPos + 4
|
||||
// If normalizedStart falls within this expanded tab, snap to origPos
|
||||
if (normPos < normalizedStart && nextNormPos > normalizedStart && origStart === -1) {
|
||||
origStart = origPos
|
||||
}
|
||||
if (normPos < normalizedStart + normalizedLength && nextNormPos > normalizedStart + normalizedLength && origEnd === -1) {
|
||||
origEnd = origPos + 1
|
||||
}
|
||||
normPos = nextNormPos
|
||||
origPos++
|
||||
} else {
|
||||
normPos++
|
||||
origPos++
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if we couldn't map precisely, use character-count heuristic
|
||||
if (origStart === -1) origStart = 0
|
||||
if (origEnd === -1) {
|
||||
// Approximate: use the ratio of original to normalized length
|
||||
const ratio = fileContent.length / normalizedFile.length
|
||||
origEnd = Math.round(origStart + normalizedLength * ratio)
|
||||
}
|
||||
|
||||
return fileContent.substring(origStart, origEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* When old_string matched via quote normalization (curly quotes in file,
|
||||
* straight quotes from model), apply the same curly quote style to new_string
|
||||
|
||||
@@ -77,6 +77,8 @@ export type Props = {
|
||||
lastThinkingBlockId?: string | null
|
||||
/** UUID of the latest user bash output message (for auto-expanding) */
|
||||
latestBashOutputUUID?: string | null
|
||||
/** Whether to collapse diff display for this message */
|
||||
shouldCollapseDiffs?: boolean
|
||||
}
|
||||
|
||||
function MessageImpl({
|
||||
@@ -99,6 +101,7 @@ function MessageImpl({
|
||||
isUserContinuation = false,
|
||||
lastThinkingBlockId,
|
||||
latestBashOutputUUID,
|
||||
shouldCollapseDiffs,
|
||||
}: Props): React.ReactNode {
|
||||
switch (message.type) {
|
||||
case 'attachment':
|
||||
@@ -181,6 +184,7 @@ function MessageImpl({
|
||||
isUserContinuation={isUserContinuation}
|
||||
lookups={lookups}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -293,6 +297,7 @@ function UserMessage({
|
||||
isUserContinuation,
|
||||
lookups,
|
||||
isTranscriptMode,
|
||||
shouldCollapseDiffs,
|
||||
}: {
|
||||
message: NormalizedUserMessage
|
||||
addMargin: boolean
|
||||
@@ -309,6 +314,7 @@ function UserMessage({
|
||||
isUserContinuation: boolean
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
isTranscriptMode: boolean
|
||||
shouldCollapseDiffs?: boolean
|
||||
}): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
switch (param.type) {
|
||||
@@ -344,6 +350,7 @@ function UserMessage({
|
||||
verbose={verbose}
|
||||
width={columns - 5}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
|
||||
@@ -55,6 +55,7 @@ export type Props = {
|
||||
columns: number
|
||||
isLoading: boolean
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
shouldCollapseDiffs?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,6 +142,7 @@ function MessageRowImpl({
|
||||
columns,
|
||||
isLoading,
|
||||
lookups,
|
||||
shouldCollapseDiffs,
|
||||
}: Props): React.ReactNode {
|
||||
const isTranscriptMode = screen === 'transcript'
|
||||
const isGrouped = msg.type === 'grouped_tool_use'
|
||||
@@ -221,6 +223,7 @@ function MessageRowImpl({
|
||||
isUserContinuation={isUserContinuation}
|
||||
lastThinkingBlockId={lastThinkingBlockId}
|
||||
latestBashOutputUUID={latestBashOutputUUID}
|
||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
||||
/>
|
||||
)
|
||||
// OffscreenFreeze: the outer React.memo already bails for static messages,
|
||||
|
||||
@@ -814,6 +814,12 @@ const MessagesImpl = ({
|
||||
streamingToolUseIDs,
|
||||
))
|
||||
|
||||
// Collapse diffs for messages beyond the latest N messages.
|
||||
// verbose (ctrl+o) overrides and always shows full diffs.
|
||||
const DIFF_COLLAPSE_DISTANCE = 0
|
||||
const shouldCollapseDiffs =
|
||||
renderableMessages.length - 1 - index > DIFF_COLLAPSE_DISTANCE
|
||||
|
||||
const k = messageKey(msg)
|
||||
const row = (
|
||||
<MessageRow
|
||||
@@ -838,6 +844,7 @@ const MessagesImpl = ({
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
lookups={lookups}
|
||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type Props = {
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
shouldCollapseDiffs?: boolean
|
||||
}
|
||||
|
||||
export function UserToolResultMessage({
|
||||
@@ -39,6 +40,7 @@ export function UserToolResultMessage({
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode,
|
||||
shouldCollapseDiffs,
|
||||
}: Props): React.ReactNode {
|
||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups)
|
||||
if (!toolUse) {
|
||||
@@ -96,6 +98,7 @@ export function UserToolResultMessage({
|
||||
verbose={verbose}
|
||||
width={width}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type Props = {
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
shouldCollapseDiffs?: boolean
|
||||
}
|
||||
|
||||
export function UserToolSuccessMessage({
|
||||
@@ -46,6 +47,7 @@ export function UserToolSuccessMessage({
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode,
|
||||
shouldCollapseDiffs,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
// Hook stays inside feature() ternary so external builds don't pay a
|
||||
@@ -83,12 +85,16 @@ export function UserToolSuccessMessage({
|
||||
}
|
||||
const toolResult = parsedOutput?.data ?? message.toolUseResult
|
||||
|
||||
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
||||
const effectiveStyle =
|
||||
shouldCollapseDiffs && !verbose ? 'condensed' : style
|
||||
|
||||
const renderedMessage =
|
||||
tool.renderToolResultMessage?.(
|
||||
toolResult as never,
|
||||
filterToolProgressMessages(progressMessagesForMessage),
|
||||
{
|
||||
style,
|
||||
style: effectiveStyle,
|
||||
theme,
|
||||
tools,
|
||||
verbose,
|
||||
|
||||
@@ -6907,6 +6907,9 @@ async function logTenguInit({
|
||||
allowDangerouslySkipPermissionsPassed,
|
||||
thinkingType:
|
||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(thinkingConfig.type === "enabled" && {
|
||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
||||
}),
|
||||
...(systemPromptFlag && {
|
||||
systemPromptFlag:
|
||||
systemPromptFlag as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
|
||||
@@ -1776,6 +1776,10 @@ async function* queryModel(
|
||||
// captures only primitives instead of paramsFromContext's full closure scope
|
||||
// (messagesForAPI, system, allTools, betas — the entire request-building
|
||||
// context), which would otherwise be pinned until the promise resolves.
|
||||
// Also capture thinking params for Langfuse observability.
|
||||
// Pass the entire thinking config object so all fields (type, budget_tokens,
|
||||
// and any future additions) flow through without cherry-picking.
|
||||
let langfuseThinking: BetaMessageStreamParams['thinking'] | undefined
|
||||
{
|
||||
const queryParams = paramsFromContext({
|
||||
model: options.model,
|
||||
@@ -1783,8 +1787,10 @@ async function* queryModel(
|
||||
})
|
||||
const logMessagesLength = queryParams.messages.length
|
||||
const logBetas = useBetas ? (queryParams.betas ?? []) : []
|
||||
const logThinkingType = queryParams.thinking?.type ?? 'disabled'
|
||||
const logEffortValue = queryParams.output_config?.effort
|
||||
if (queryParams.thinking && queryParams.thinking.type !== 'disabled') {
|
||||
langfuseThinking = queryParams.thinking
|
||||
}
|
||||
void options.getToolPermissionContext().then(permissionContext => {
|
||||
logAPIQuery({
|
||||
model: options.model,
|
||||
@@ -1794,7 +1800,7 @@ async function* queryModel(
|
||||
permissionMode: permissionContext.mode,
|
||||
querySource: options.querySource,
|
||||
queryTracking: options.queryTracking,
|
||||
thinkingType: logThinkingType,
|
||||
thinkingConfig,
|
||||
effortValue: logEffortValue,
|
||||
fastMode: isFastMode,
|
||||
previousRequestId,
|
||||
@@ -2545,6 +2551,9 @@ async function* queryModel(
|
||||
maxOutputTokens,
|
||||
thinkingType:
|
||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(thinkingConfig.type === 'enabled' && {
|
||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
||||
}),
|
||||
fallback_disabled: true,
|
||||
request_id: (streamRequestId ??
|
||||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
@@ -2577,6 +2586,9 @@ async function* queryModel(
|
||||
maxOutputTokens,
|
||||
thinkingType:
|
||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(thinkingConfig.type === 'enabled' && {
|
||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
||||
}),
|
||||
fallback_disabled: false,
|
||||
request_id: (streamRequestId ??
|
||||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
@@ -2693,6 +2705,9 @@ async function* queryModel(
|
||||
maxOutputTokens,
|
||||
thinkingType:
|
||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(thinkingConfig.type === 'enabled' && {
|
||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
||||
}),
|
||||
request_id:
|
||||
failedRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
fallback_cause:
|
||||
@@ -2925,6 +2940,7 @@ async function* queryModel(
|
||||
endTime: new Date(),
|
||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||
thinking: langfuseThinking,
|
||||
})
|
||||
|
||||
void options.getToolPermissionContext().then(permissionContext => {
|
||||
|
||||
@@ -193,6 +193,15 @@ export async function* queryModelGemini(
|
||||
endTime: new Date(),
|
||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||
thinking:
|
||||
thinkingConfig.type !== 'disabled'
|
||||
? {
|
||||
type: thinkingConfig.type,
|
||||
...(thinkingConfig.type === 'enabled' && {
|
||||
budgetTokens: thinkingConfig.budgetTokens,
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getAPIProviderForStatsig } from 'src/utils/model/providers.js'
|
||||
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import { logOTelEvent } from 'src/utils/telemetry/events.js'
|
||||
import type { ThinkingConfig } from 'src/utils/thinking.js'
|
||||
import {
|
||||
endLLMRequestSpan,
|
||||
isBetaTracingEnabled,
|
||||
@@ -176,7 +177,7 @@ export function logAPIQuery({
|
||||
permissionMode,
|
||||
querySource,
|
||||
queryTracking,
|
||||
thinkingType,
|
||||
thinkingConfig,
|
||||
effortValue,
|
||||
fastMode,
|
||||
previousRequestId,
|
||||
@@ -188,11 +189,13 @@ export function logAPIQuery({
|
||||
permissionMode?: PermissionMode
|
||||
querySource: string
|
||||
queryTracking?: QueryChainTracking
|
||||
thinkingType?: 'adaptive' | 'enabled' | 'disabled'
|
||||
thinkingConfig?: ThinkingConfig
|
||||
effortValue?: EffortLevel | null
|
||||
fastMode?: boolean
|
||||
previousRequestId?: string | null
|
||||
}): void {
|
||||
const thinkingType = thinkingConfig?.type ?? 'disabled'
|
||||
const thinkingBudgetTokens = thinkingConfig?.type === 'enabled' ? thinkingConfig.budgetTokens : undefined
|
||||
logEvent('tengu_api_query', {
|
||||
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
messagesLength,
|
||||
@@ -219,6 +222,9 @@ export function logAPIQuery({
|
||||
: {}),
|
||||
thinkingType:
|
||||
thinkingType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...(thinkingBudgetTokens !== undefined && {
|
||||
thinkingBudgetTokens,
|
||||
}),
|
||||
effortValue:
|
||||
effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
fastMode,
|
||||
|
||||
@@ -418,6 +418,7 @@ export async function* queryModelOpenAI(
|
||||
endTime: new Date(),
|
||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||
...(enableThinking && { thinking: { type: 'enabled' } }),
|
||||
})
|
||||
|
||||
// Safety: if stream ended without message_stop, assemble and yield whatever we have
|
||||
|
||||
@@ -78,6 +78,16 @@ export function recordLLMObservation(
|
||||
endTime?: Date
|
||||
completionStartTime?: Date
|
||||
tools?: unknown
|
||||
/** Thinking depth configuration used for this request.
|
||||
* Accepts the full API thinking config object. Fields:
|
||||
* - type: thinking mode ("enabled", "adaptive", "disabled")
|
||||
* - budget_tokens (snake_case, from Anthropic API) or budgetTokens (camelCase)
|
||||
*/
|
||||
thinking?: {
|
||||
type: string
|
||||
budget_tokens?: number
|
||||
budgetTokens?: number
|
||||
}
|
||||
},
|
||||
): void {
|
||||
if (!rootSpan || !isLangfuseEnabled()) return
|
||||
@@ -97,6 +107,7 @@ export function recordLLMObservation(
|
||||
metadata: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
...(params.thinking && { thinking: params.thinking }),
|
||||
},
|
||||
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
|
||||
},
|
||||
|
||||
@@ -354,6 +354,7 @@ export async function countTokensViaHaikuFallback(
|
||||
},
|
||||
startTime: new Date(apiStart),
|
||||
endTime: new Date(),
|
||||
...(containsThinking && { thinking: { type: 'enabled', budgetTokens: TOKEN_COUNT_THINKING_BUDGET } }),
|
||||
})
|
||||
endTrace(langfuseTrace)
|
||||
|
||||
|
||||
@@ -294,6 +294,12 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
||||
startTime: new Date(start),
|
||||
endTime: new Date(),
|
||||
...(tools && { tools: convertToolsToLangfuse(tools as unknown[]) }),
|
||||
...(thinkingConfig && thinkingConfig.type !== 'disabled' && {
|
||||
thinking: {
|
||||
type: thinkingConfig.type,
|
||||
...(thinkingConfig.type === 'enabled' && { budgetTokens: thinkingConfig.budget_tokens }),
|
||||
},
|
||||
}),
|
||||
})
|
||||
endTrace(langfuseTrace)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user