mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 07:45:52 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -230,7 +230,11 @@ import { getInitializationStatus } from '../lsp/manager.js'
|
||||
import { isToolFromMcpServer } from '../mcp/utils.js'
|
||||
import { recordLLMObservation } from '../langfuse/index.js'
|
||||
import type { LangfuseSpan } from '../langfuse/index.js'
|
||||
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../langfuse/convert.js'
|
||||
import {
|
||||
convertMessagesToLangfuse,
|
||||
convertOutputToLangfuse,
|
||||
convertToolsToLangfuse,
|
||||
} from '../langfuse/convert.js'
|
||||
import { withStreamingVCR, withVCR } from '../vcr.js'
|
||||
import { CLIENT_REQUEST_ID_HEADER, getAnthropicClient } from './client.js'
|
||||
import {
|
||||
@@ -442,7 +446,7 @@ function configureEffortParams(
|
||||
betas.push(EFFORT_BETA_HEADER)
|
||||
} else if (typeof effortValue === 'string') {
|
||||
// Send string effort level as is
|
||||
outputConfig.effort = effortValue as "high" | "medium" | "low" | "max"
|
||||
outputConfig.effort = effortValue as 'high' | 'medium' | 'low' | 'max'
|
||||
betas.push(EFFORT_BETA_HEADER)
|
||||
} else if (process.env.USER_TYPE === 'ant') {
|
||||
// Numeric effort override - ant-only (uses anthropic_internal)
|
||||
@@ -615,7 +619,8 @@ export function userMessageToMessageParam(
|
||||
role: 'user',
|
||||
content: (Array.isArray(message.message!.content)
|
||||
? [...message.message!.content]
|
||||
: message.message!.content) as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlockParam[],
|
||||
: message.message!
|
||||
.content) as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlockParam[],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +671,9 @@ export function assistantMessageToMessageParam(
|
||||
content:
|
||||
typeof message.message!.content === 'string'
|
||||
? message.message!.content
|
||||
: message.message!.content!.map(stripGeminiProviderMetadata) as BetaContentBlockParam[],
|
||||
: (message.message!.content!.map(
|
||||
stripGeminiProviderMetadata,
|
||||
) as BetaContentBlockParam[]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,10 +688,8 @@ function stripGeminiProviderMetadata<T extends BetaContentBlockParam | string>(
|
||||
}
|
||||
|
||||
const obj = contentBlock as unknown as Record<string, unknown>
|
||||
const {
|
||||
_geminiThoughtSignature: _unusedGeminiThoughtSignature,
|
||||
...rest
|
||||
} = obj
|
||||
const { _geminiThoughtSignature: _unusedGeminiThoughtSignature, ...rest } =
|
||||
obj
|
||||
return rest as unknown as T
|
||||
}
|
||||
|
||||
@@ -1343,7 +1348,13 @@ async function* queryModel(
|
||||
// OpenAI emulates Anthropic's dynamic tool loading client-side. It needs
|
||||
// the full tool pool so ToolSearchTool can search deferred MCP tools that
|
||||
// were intentionally filtered out of the initial API tool list above.
|
||||
yield* queryModelOpenAI(messagesForAPI, systemPrompt, tools, signal, options)
|
||||
yield* queryModelOpenAI(
|
||||
messagesForAPI,
|
||||
systemPrompt,
|
||||
tools,
|
||||
signal,
|
||||
options,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1362,7 +1373,13 @@ async function* queryModel(
|
||||
|
||||
if (getAPIProvider() === 'grok') {
|
||||
const { queryModelGrok } = await import('./grok/index.js')
|
||||
yield* queryModelGrok(messagesForAPI, systemPrompt, filteredTools, signal, options)
|
||||
yield* queryModelGrok(
|
||||
messagesForAPI,
|
||||
systemPrompt,
|
||||
filteredTools,
|
||||
signal,
|
||||
options,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2127,7 +2144,8 @@ async function* queryModel(
|
||||
})
|
||||
throw new Error('Content block is not a connector_text block')
|
||||
}
|
||||
;(contentBlock as { connector_text: string }).connector_text += delta.connector_text
|
||||
;(contentBlock as { connector_text: string }).connector_text +=
|
||||
delta.connector_text
|
||||
} else {
|
||||
switch (delta.type) {
|
||||
case 'citations_delta':
|
||||
@@ -2206,7 +2224,8 @@ async function* queryModel(
|
||||
})
|
||||
throw new Error('Content block is not a thinking block')
|
||||
}
|
||||
;(contentBlock as { thinking: string }).thinking += delta.thinking
|
||||
;(contentBlock as { thinking: string }).thinking +=
|
||||
delta.thinking
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2298,7 +2317,10 @@ async function* queryModel(
|
||||
}
|
||||
|
||||
// Update cost
|
||||
const costUSDForPart = calculateUSDCost(resolvedModel, usage as unknown as BetaUsage)
|
||||
const costUSDForPart = calculateUSDCost(
|
||||
resolvedModel,
|
||||
usage as unknown as BetaUsage,
|
||||
)
|
||||
costUSD += addToTotalSessionCost(
|
||||
costUSDForPart,
|
||||
usage as unknown as BetaUsage,
|
||||
@@ -2887,10 +2909,14 @@ async function* queryModel(
|
||||
// message_delta handler before any yield. Fallback pushes to newMessages
|
||||
// then yields, so tracking must be here to survive .return() at the yield.
|
||||
if (fallbackMessage) {
|
||||
const fallbackUsage = fallbackMessage.message.usage as BetaMessageDeltaUsage
|
||||
const fallbackUsage = fallbackMessage.message
|
||||
.usage as BetaMessageDeltaUsage
|
||||
usage = updateUsage(EMPTY_USAGE, fallbackUsage)
|
||||
stopReason = fallbackMessage.message.stop_reason as BetaStopReason
|
||||
const fallbackCost = calculateUSDCost(resolvedModel, fallbackUsage as unknown as BetaUsage)
|
||||
const fallbackCost = calculateUSDCost(
|
||||
resolvedModel,
|
||||
fallbackUsage as unknown as BetaUsage,
|
||||
)
|
||||
costUSD += addToTotalSessionCost(
|
||||
fallbackCost,
|
||||
fallbackUsage as unknown as BetaUsage,
|
||||
@@ -2946,7 +2972,9 @@ async function* queryModel(
|
||||
void options.getToolPermissionContext().then(permissionContext => {
|
||||
logAPISuccessAndDuration({
|
||||
model:
|
||||
(newMessages[0]?.message.model as string | undefined) ?? partialMessage?.model ?? options.model,
|
||||
(newMessages[0]?.message.model as string | undefined) ??
|
||||
partialMessage?.model ??
|
||||
options.model,
|
||||
preNormalizedModel: options.model,
|
||||
usage,
|
||||
start,
|
||||
|
||||
@@ -19,9 +19,20 @@ import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
import type { ThinkingConfig } from '../../../utils/thinking.js'
|
||||
import type { Options } from '../claude.js'
|
||||
import { recordLLMObservation } from '../../../services/langfuse/tracing.js'
|
||||
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../../../services/langfuse/convert.js'
|
||||
import {
|
||||
convertMessagesToLangfuse,
|
||||
convertOutputToLangfuse,
|
||||
convertToolsToLangfuse,
|
||||
} from '../../../services/langfuse/convert.js'
|
||||
import { streamGeminiGenerateContent } from './client.js'
|
||||
import { anthropicMessagesToGemini, resolveGeminiModel, adaptGeminiStreamToAnthropic, anthropicToolsToGemini, anthropicToolChoiceToGemini, GEMINI_THOUGHT_SIGNATURE_FIELD } from '@ant/model-provider'
|
||||
import {
|
||||
anthropicMessagesToGemini,
|
||||
resolveGeminiModel,
|
||||
adaptGeminiStreamToAnthropic,
|
||||
anthropicToolsToGemini,
|
||||
anthropicToolChoiceToGemini,
|
||||
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
||||
} from '@ant/model-provider'
|
||||
|
||||
export async function* queryModelGemini(
|
||||
messages: Message[],
|
||||
@@ -209,7 +220,9 @@ export async function* queryModelGemini(
|
||||
yield createAssistantAPIErrorMessage({
|
||||
content: `API Error: ${errorMessage}`,
|
||||
apiError: 'api_error',
|
||||
error: (error instanceof Error ? error : new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
error: (error instanceof Error
|
||||
? error
|
||||
: new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, mock } from 'bun:test'
|
||||
|
||||
// Defensive: agent.test.ts can corrupt Bun's src/* path alias at runtime.
|
||||
mock.module('src/utils/proxy.js', () => ({
|
||||
getProxyFetchOptions: () => ({} as any),
|
||||
getProxyFetchOptions: () => ({}) as any,
|
||||
}))
|
||||
|
||||
import { getGrokClient, clearGrokClientCache } from '../client.js'
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js'
|
||||
import type {
|
||||
Message,
|
||||
StreamEvent,
|
||||
SystemAPIErrorMessage,
|
||||
AssistantMessage,
|
||||
} from '../../../types/message.js'
|
||||
import type { Tools } from '../../../Tool.js'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
} from 'openai/resources/chat/completions/completions.mjs'
|
||||
import { getGrokClient } from './client.js'
|
||||
import { anthropicMessagesToOpenAI, anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI, adaptOpenAIStreamToAnthropic, resolveGrokModel } from '@ant/model-provider'
|
||||
import {
|
||||
anthropicMessagesToOpenAI,
|
||||
anthropicToolsToOpenAI,
|
||||
anthropicToolChoiceToOpenAI,
|
||||
adaptOpenAIStreamToAnthropic,
|
||||
resolveGrokModel,
|
||||
} from '@ant/model-provider'
|
||||
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
|
||||
import type { SDKAssistantMessageError } from '../../../entrypoints/agentSdkTypes.js'
|
||||
import { toolToAPISchema } from '../../../utils/api.js'
|
||||
@@ -15,7 +26,11 @@ import { logForDebugging } from '../../../utils/debug.js'
|
||||
import { addToTotalSessionCost } from '../../../cost-tracker.js'
|
||||
import { calculateUSDCost } from '../../../utils/modelCost.js'
|
||||
import { recordLLMObservation } from '../../../services/langfuse/tracing.js'
|
||||
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../../../services/langfuse/convert.js'
|
||||
import {
|
||||
convertMessagesToLangfuse,
|
||||
convertOutputToLangfuse,
|
||||
convertToolsToLangfuse,
|
||||
} from '../../../services/langfuse/convert.js'
|
||||
import type { Options } from '../claude.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
import {
|
||||
@@ -56,11 +71,16 @@ export async function* queryModelGrok(
|
||||
const standardTools = toolSchemas.filter(
|
||||
(t): t is BetaToolUnion & { type: string } => {
|
||||
const anyT = t as unknown as Record<string, unknown>
|
||||
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
||||
return (
|
||||
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const openaiMessages = anthropicMessagesToOpenAI(messagesForAPI, systemPrompt)
|
||||
const openaiMessages = anthropicMessagesToOpenAI(
|
||||
messagesForAPI,
|
||||
systemPrompt,
|
||||
)
|
||||
const openaiTools = anthropicToolsToOpenAI(standardTools)
|
||||
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
|
||||
|
||||
@@ -70,7 +90,9 @@ export async function* queryModelGrok(
|
||||
source: options.querySource,
|
||||
})
|
||||
|
||||
logForDebugging(`[Grok] Calling model=${grokModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`)
|
||||
logForDebugging(
|
||||
`[Grok] Calling model=${grokModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
|
||||
)
|
||||
|
||||
const stream = await client.chat.completions.create(
|
||||
{
|
||||
@@ -91,7 +113,10 @@ export async function* queryModelGrok(
|
||||
},
|
||||
)
|
||||
|
||||
const adaptedStream = adaptOpenAIStreamToAnthropic(stream as AsyncIterable<ChatCompletionChunk>, grokModel)
|
||||
const adaptedStream = adaptOpenAIStreamToAnthropic(
|
||||
stream as AsyncIterable<ChatCompletionChunk>,
|
||||
grokModel,
|
||||
)
|
||||
|
||||
const contentBlocks: Record<number, any> = {}
|
||||
const collectedMessages: AssistantMessage[] = []
|
||||
@@ -111,7 +136,7 @@ export async function* queryModelGrok(
|
||||
partialMessage = (event as any).message
|
||||
ttftMs = Date.now() - start
|
||||
if ((event as any).message?.usage) {
|
||||
usage = { ...usage, ...((event as any).message.usage) }
|
||||
usage = { ...usage, ...(event as any).message.usage }
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -175,7 +200,10 @@ export async function* queryModelGrok(
|
||||
break
|
||||
}
|
||||
|
||||
if (event.type === 'message_stop' && usage.input_tokens + usage.output_tokens > 0) {
|
||||
if (
|
||||
event.type === 'message_stop' &&
|
||||
usage.input_tokens + usage.output_tokens > 0
|
||||
) {
|
||||
const costUSD = calculateUSDCost(grokModel, usage as any)
|
||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
||||
}
|
||||
@@ -210,7 +238,9 @@ export async function* queryModelGrok(
|
||||
yield createAssistantAPIErrorMessage({
|
||||
content: `API Error: ${errorMessage}`,
|
||||
apiError: 'api_error',
|
||||
error: (error instanceof Error ? error : new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
error: (error instanceof Error
|
||||
? error
|
||||
: new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,8 @@ export function logAPIQuery({
|
||||
previousRequestId?: string | null
|
||||
}): void {
|
||||
const thinkingType = thinkingConfig?.type ?? 'disabled'
|
||||
const thinkingBudgetTokens = thinkingConfig?.type === 'enabled' ? thinkingConfig.budgetTokens : undefined
|
||||
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,
|
||||
@@ -662,7 +663,9 @@ export function logAPISuccessAndDuration({
|
||||
let connectorCount = 0
|
||||
|
||||
for (const msg of newMessages) {
|
||||
const contentArr = Array.isArray(msg.message.content) ? msg.message.content : []
|
||||
const contentArr = Array.isArray(msg.message.content)
|
||||
? msg.message.content
|
||||
: []
|
||||
for (const block of contentArr) {
|
||||
if (typeof block === 'string') continue
|
||||
if (block.type === 'text') {
|
||||
@@ -670,14 +673,19 @@ export function logAPISuccessAndDuration({
|
||||
} else if (feature('CONNECTOR_TEXT') && isConnectorTextBlock(block)) {
|
||||
connectorCount++
|
||||
} else if (block.type === 'thinking') {
|
||||
thinkingLen += (block as { type: 'thinking'; thinking: string }).thinking.length
|
||||
thinkingLen += (block as { type: 'thinking'; thinking: string })
|
||||
.thinking.length
|
||||
} else if (
|
||||
block.type === 'tool_use' ||
|
||||
block.type === 'server_tool_use' ||
|
||||
(block.type as string) === 'mcp_tool_use'
|
||||
) {
|
||||
const inputLen = jsonStringify((block as { input: unknown }).input).length
|
||||
const sanitizedName = sanitizeToolNameForAnalytics((block as { name: string }).name)
|
||||
const inputLen = jsonStringify(
|
||||
(block as { input: unknown }).input,
|
||||
).length
|
||||
const sanitizedName = sanitizeToolNameForAnalytics(
|
||||
(block as { name: string }).name,
|
||||
)
|
||||
toolLengths[sanitizedName] =
|
||||
(toolLengths[sanitizedName] ?? 0) + inputLen
|
||||
hasToolUse = true
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||
import { isOpenAIThinkingEnabled, buildOpenAIRequestBody } from '../requestBody.js'
|
||||
import {
|
||||
isOpenAIThinkingEnabled,
|
||||
buildOpenAIRequestBody,
|
||||
} from '../requestBody.js'
|
||||
|
||||
describe('isOpenAIThinkingEnabled', () => {
|
||||
const originalEnv = {
|
||||
@@ -81,7 +84,9 @@ describe('isOpenAIThinkingEnabled', () => {
|
||||
})
|
||||
|
||||
test('returns true when model name is namespaced for deepseek-reasoner', () => {
|
||||
expect(isOpenAIThinkingEnabled('TokenService/deepseek-reasoner')).toBe(true)
|
||||
expect(isOpenAIThinkingEnabled('TokenService/deepseek-reasoner')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('returns true when model name is "deepseek-v3.2"', () => {
|
||||
@@ -185,14 +190,20 @@ describe('buildOpenAIRequestBody — thinking params', () => {
|
||||
})
|
||||
|
||||
test('does NOT include thinking params when disabled', () => {
|
||||
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: false })
|
||||
const body = buildOpenAIRequestBody({
|
||||
...baseParams,
|
||||
enableThinking: false,
|
||||
})
|
||||
expect(body.thinking).toBeUndefined()
|
||||
expect(body.enable_thinking).toBeUndefined()
|
||||
expect(body.chat_template_kwargs).toBeUndefined()
|
||||
})
|
||||
|
||||
test('always includes stream and stream_options', () => {
|
||||
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: false })
|
||||
const body = buildOpenAIRequestBody({
|
||||
...baseParams,
|
||||
enableThinking: false,
|
||||
})
|
||||
expect(body.stream).toBe(true)
|
||||
expect(body.stream_options).toEqual({ include_usage: true })
|
||||
})
|
||||
@@ -216,7 +227,10 @@ describe('buildOpenAIRequestBody — thinking params', () => {
|
||||
})
|
||||
|
||||
test('excludes temperature when thinking is off and no override', () => {
|
||||
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: false })
|
||||
const body = buildOpenAIRequestBody({
|
||||
...baseParams,
|
||||
enableThinking: false,
|
||||
})
|
||||
expect(body.temperature).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -232,8 +246,11 @@ describe('buildOpenAIRequestBody — thinking params', () => {
|
||||
})
|
||||
|
||||
test('excludes tools when empty', () => {
|
||||
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: false })
|
||||
const body = buildOpenAIRequestBody({
|
||||
...baseParams,
|
||||
enableThinking: false,
|
||||
})
|
||||
expect(body.tools).toBeUndefined()
|
||||
expect(body.tool_choice).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,8 +56,12 @@ export function getOpenAIClient(options?: {
|
||||
maxRetries: options?.maxRetries ?? 0,
|
||||
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10),
|
||||
dangerouslyAllowBrowser: true,
|
||||
...(process.env.OPENAI_ORG_ID && { organization: process.env.OPENAI_ORG_ID }),
|
||||
...(process.env.OPENAI_PROJECT_ID && { project: process.env.OPENAI_PROJECT_ID }),
|
||||
...(process.env.OPENAI_ORG_ID && {
|
||||
organization: process.env.OPENAI_ORG_ID,
|
||||
}),
|
||||
...(process.env.OPENAI_PROJECT_ID && {
|
||||
project: process.env.OPENAI_PROJECT_ID,
|
||||
}),
|
||||
fetchOptions: getProxyFetchOptions({ forAnthropicAPI: false }),
|
||||
fetch: wrappedFetch,
|
||||
})
|
||||
|
||||
@@ -10,11 +10,15 @@ import type {
|
||||
import type { AgentId } from '../../../types/ids.js'
|
||||
import type { Tools } from '../../../Tool.js'
|
||||
import type { Stream } from 'openai/streaming.mjs'
|
||||
import type {
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
} from 'openai/resources/chat/completions/completions.mjs'
|
||||
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions/completions.mjs'
|
||||
import { getOpenAIClient } from './client.js'
|
||||
import { anthropicMessagesToOpenAI, resolveOpenAIModel, adaptOpenAIStreamToAnthropic, anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '@ant/model-provider'
|
||||
import {
|
||||
anthropicMessagesToOpenAI,
|
||||
resolveOpenAIModel,
|
||||
adaptOpenAIStreamToAnthropic,
|
||||
anthropicToolsToOpenAI,
|
||||
anthropicToolChoiceToOpenAI,
|
||||
} from '@ant/model-provider'
|
||||
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
|
||||
import { toolToAPISchema } from '../../../utils/api.js'
|
||||
import {
|
||||
@@ -24,10 +28,22 @@ import {
|
||||
import { logForDebugging } from '../../../utils/debug.js'
|
||||
import { addToTotalSessionCost } from '../../../cost-tracker.js'
|
||||
import { calculateUSDCost } from '../../../utils/modelCost.js'
|
||||
import { isOpenAIThinkingEnabled, resolveOpenAIMaxTokens, buildOpenAIRequestBody } from './requestBody.js'
|
||||
import {
|
||||
isOpenAIThinkingEnabled,
|
||||
resolveOpenAIMaxTokens,
|
||||
buildOpenAIRequestBody,
|
||||
} from './requestBody.js'
|
||||
import { recordLLMObservation } from '../../../services/langfuse/tracing.js'
|
||||
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../../../services/langfuse/convert.js'
|
||||
export { isOpenAIThinkingEnabled, resolveOpenAIMaxTokens, buildOpenAIRequestBody }
|
||||
import {
|
||||
convertMessagesToLangfuse,
|
||||
convertOutputToLangfuse,
|
||||
convertToolsToLangfuse,
|
||||
} from '../../../services/langfuse/convert.js'
|
||||
export {
|
||||
isOpenAIThinkingEnabled,
|
||||
resolveOpenAIMaxTokens,
|
||||
buildOpenAIRequestBody,
|
||||
}
|
||||
import { getModelMaxOutputTokens } from '../../../utils/context.js'
|
||||
import type { Options } from '../claude.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
@@ -81,7 +97,9 @@ function prependDeferredToolListIfNeeded(
|
||||
]
|
||||
}
|
||||
|
||||
function isOpenAIConvertibleMessage(msg: Message): msg is AssistantMessage | UserMessage {
|
||||
function isOpenAIConvertibleMessage(
|
||||
msg: Message,
|
||||
): msg is AssistantMessage | UserMessage {
|
||||
return msg.type === 'assistant' || msg.type === 'user'
|
||||
}
|
||||
|
||||
@@ -95,11 +113,24 @@ function assembleFinalAssistantOutputs(params: {
|
||||
contentBlocks: Record<number, any>
|
||||
tools: Tools
|
||||
agentId: string | undefined
|
||||
usage: { input_tokens: number; output_tokens: number; cache_creation_input_tokens: number; cache_read_input_tokens: number }
|
||||
usage: {
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens: number
|
||||
cache_read_input_tokens: number
|
||||
}
|
||||
stopReason: string | null
|
||||
maxTokens: number
|
||||
}): (AssistantMessage | SystemAPIErrorMessage)[] {
|
||||
const { partialMessage, contentBlocks, tools, agentId, usage, stopReason, maxTokens } = params
|
||||
const {
|
||||
partialMessage,
|
||||
contentBlocks,
|
||||
tools,
|
||||
agentId,
|
||||
usage,
|
||||
stopReason,
|
||||
maxTokens,
|
||||
} = params
|
||||
const outputs: (AssistantMessage | SystemAPIErrorMessage)[] = []
|
||||
|
||||
const allBlocks = Object.keys(contentBlocks)
|
||||
@@ -111,7 +142,11 @@ function assembleFinalAssistantOutputs(params: {
|
||||
outputs.push({
|
||||
message: {
|
||||
...partialMessage,
|
||||
content: normalizeContentFromAPI(allBlocks, tools, agentId as AgentId | undefined),
|
||||
content: normalizeContentFromAPI(
|
||||
allBlocks,
|
||||
tools,
|
||||
agentId as AgentId | undefined,
|
||||
),
|
||||
usage,
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
@@ -124,12 +159,15 @@ function assembleFinalAssistantOutputs(params: {
|
||||
}
|
||||
|
||||
if (stopReason === 'max_tokens') {
|
||||
outputs.push(createAssistantAPIErrorMessage({
|
||||
content: `Output truncated: response exceeded the ${maxTokens} token limit. ` +
|
||||
`Set OPENAI_MAX_TOKENS or CLAUDE_CODE_MAX_OUTPUT_TOKENS to override.`,
|
||||
apiError: 'max_output_tokens',
|
||||
error: 'max_output_tokens',
|
||||
}))
|
||||
outputs.push(
|
||||
createAssistantAPIErrorMessage({
|
||||
content:
|
||||
`Output truncated: response exceeded the ${maxTokens} token limit. ` +
|
||||
`Set OPENAI_MAX_TOKENS or CLAUDE_CODE_MAX_OUTPUT_TOKENS to override.`,
|
||||
apiError: 'max_output_tokens',
|
||||
error: 'max_output_tokens',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return outputs
|
||||
@@ -217,7 +255,9 @@ export async function* queryModelOpenAI(
|
||||
|
||||
// 8. Convert messages and tools to OpenAI format
|
||||
const enableThinking = isOpenAIThinkingEnabled(openaiModel)
|
||||
const openAIConvertibleMessages = messagesForAPI.filter(isOpenAIConvertibleMessage)
|
||||
const openAIConvertibleMessages = messagesForAPI.filter(
|
||||
isOpenAIConvertibleMessage,
|
||||
)
|
||||
const messagesWithDeferredToolList = prependDeferredToolListIfNeeded(
|
||||
openAIConvertibleMessages,
|
||||
tools,
|
||||
@@ -264,7 +304,10 @@ export async function* queryModelOpenAI(
|
||||
// 3. CLAUDE_CODE_MAX_OUTPUT_TOKENS env var (generic override)
|
||||
// 4. upperLimit default (64000)
|
||||
const { upperLimit } = getModelMaxOutputTokens(openaiModel)
|
||||
const maxTokens = resolveOpenAIMaxTokens(upperLimit, options.maxOutputTokensOverride)
|
||||
const maxTokens = resolveOpenAIMaxTokens(
|
||||
upperLimit,
|
||||
options.maxOutputTokensOverride,
|
||||
)
|
||||
|
||||
// 11. Get client
|
||||
const client = getOpenAIClient({
|
||||
@@ -287,10 +330,7 @@ export async function* queryModelOpenAI(
|
||||
maxTokens,
|
||||
temperatureOverride: options.temperatureOverride,
|
||||
})
|
||||
const stream = await client.chat.completions.create(
|
||||
requestBody,
|
||||
{ signal },
|
||||
)
|
||||
const stream = await client.chat.completions.create(requestBody, { signal })
|
||||
|
||||
// 12. Convert OpenAI stream to Anthropic events, then process into
|
||||
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
||||
@@ -373,8 +413,13 @@ export async function* queryModelOpenAI(
|
||||
// here and injected so tokenCountWithEstimation() can read it.
|
||||
if (partialMessage) {
|
||||
for (const output of assembleFinalAssistantOutputs({
|
||||
partialMessage, contentBlocks, tools, agentId: options.agentId,
|
||||
usage, stopReason, maxTokens,
|
||||
partialMessage,
|
||||
contentBlocks,
|
||||
tools,
|
||||
agentId: options.agentId,
|
||||
usage,
|
||||
stopReason,
|
||||
maxTokens,
|
||||
})) {
|
||||
if (output.type === 'assistant') {
|
||||
collectedMessages.push(output)
|
||||
@@ -424,8 +469,13 @@ export async function* queryModelOpenAI(
|
||||
// Safety: if stream ended without message_stop, assemble and yield whatever we have
|
||||
if (partialMessage) {
|
||||
for (const output of assembleFinalAssistantOutputs({
|
||||
partialMessage, contentBlocks, tools, agentId: options.agentId,
|
||||
usage, stopReason, maxTokens,
|
||||
partialMessage,
|
||||
contentBlocks,
|
||||
tools,
|
||||
agentId: options.agentId,
|
||||
usage,
|
||||
stopReason,
|
||||
maxTokens,
|
||||
})) {
|
||||
yield output
|
||||
}
|
||||
@@ -436,7 +486,9 @@ export async function* queryModelOpenAI(
|
||||
yield createAssistantAPIErrorMessage({
|
||||
content: `API Error: ${errorMessage}`,
|
||||
apiError: 'api_error',
|
||||
error: (error instanceof Error ? error : new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
error: (error instanceof Error
|
||||
? error
|
||||
: new Error(String(error))) as unknown as SDKAssistantMessageError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
* thinking mode. Extracted from index.ts so tests can import them without
|
||||
* triggering heavy module side-effects (OpenAI client, stream adapter, etc.).
|
||||
*/
|
||||
import type {
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
} from 'openai/resources/chat/completions/completions.mjs'
|
||||
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions/completions.mjs'
|
||||
import { isEnvTruthy, isEnvDefinedFalsy } from '../../../utils/envUtils.js'
|
||||
|
||||
/**
|
||||
@@ -44,10 +42,16 @@ export function resolveOpenAIMaxTokens(
|
||||
upperLimit: number,
|
||||
maxOutputTokensOverride?: number,
|
||||
): number {
|
||||
return maxOutputTokensOverride
|
||||
?? (process.env.OPENAI_MAX_TOKENS ? parseInt(process.env.OPENAI_MAX_TOKENS, 10) || undefined : undefined)
|
||||
?? (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ? parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10) || undefined : undefined)
|
||||
?? upperLimit
|
||||
return (
|
||||
maxOutputTokensOverride ??
|
||||
(process.env.OPENAI_MAX_TOKENS
|
||||
? parseInt(process.env.OPENAI_MAX_TOKENS, 10) || undefined
|
||||
: undefined) ??
|
||||
(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
? parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10) || undefined
|
||||
: undefined) ??
|
||||
upperLimit
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +78,15 @@ export function buildOpenAIRequestBody(params: {
|
||||
enable_thinking?: boolean
|
||||
chat_template_kwargs?: { thinking: boolean }
|
||||
} {
|
||||
const { model, messages, tools, toolChoice, enableThinking, maxTokens, temperatureOverride } = params
|
||||
const {
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
toolChoice,
|
||||
enableThinking,
|
||||
maxTokens,
|
||||
temperatureOverride,
|
||||
} = params
|
||||
return {
|
||||
model,
|
||||
messages,
|
||||
@@ -96,8 +108,9 @@ export function buildOpenAIRequestBody(params: {
|
||||
}),
|
||||
// Only send temperature when thinking mode is off (DeepSeek ignores it anyway,
|
||||
// but other providers may respect it)
|
||||
...(!enableThinking && temperatureOverride !== undefined && {
|
||||
temperature: temperatureOverride,
|
||||
}),
|
||||
...(!enableThinking &&
|
||||
temperatureOverride !== undefined && {
|
||||
temperature: temperatureOverride,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +459,8 @@ export async function checkResponseForCacheBreak(
|
||||
// assistant message timestamp in the messages array (before the current response)
|
||||
const lastAssistantMessage = messages.findLast(m => m.type === 'assistant')
|
||||
const timeSinceLastAssistantMsg = lastAssistantMessage
|
||||
? Date.now() - new Date(lastAssistantMessage.timestamp as string | number).getTime()
|
||||
? Date.now() -
|
||||
new Date(lastAssistantMessage.timestamp as string | number).getTime()
|
||||
: null
|
||||
|
||||
// Skip the first call — no previous value to compare against
|
||||
|
||||
@@ -539,10 +539,7 @@ export function getRetryDelay(
|
||||
}
|
||||
}
|
||||
|
||||
const baseDelay = Math.min(
|
||||
BASE_DELAY_MS * 2 ** (attempt - 1),
|
||||
maxDelayMs,
|
||||
)
|
||||
const baseDelay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), maxDelayMs)
|
||||
const jitter = Math.random() * 0.25 * baseDelay
|
||||
return baseDelay + jitter
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user