mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
fix: 修复 OpenAI provider (gpt-5.4/gpt-5.3-codex等模型)下 内建mcp__plugin_weixin_weixin__reply 微信工具不可见的问题 (#359)
* fix: 修复 OpenAI provider 下 MCP 工具不可见 * docs: 补充 OpenAI MCP 工具列表注释 * fix: 修正 OpenAI Langfuse 输入记录 * refactor: 使用类型守卫收窄 Langfuse role * fix: 保留 Langfuse OpenAI 数组消息角色 * fix: 合并 Langfuse OpenAI tool_calls * fix: 修复 OpenAI Langfuse 类型检查
This commit is contained in:
@@ -184,6 +184,100 @@ describe('Langfuse integration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertMessagesToLangfuse', () => {
|
||||
test('preserves OpenAI-style messages including deferred tool announcements', async () => {
|
||||
const { convertMessagesToLangfuse } = await import('../convert.js')
|
||||
const result = convertMessagesToLangfuse([
|
||||
{
|
||||
role: 'system',
|
||||
content: 'system prompt',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content:
|
||||
'<available-deferred-tools>\nmcp__wechat__send_message\n</available-deferred-tools>',
|
||||
},
|
||||
])
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'system', content: 'system prompt' },
|
||||
{
|
||||
role: 'user',
|
||||
content:
|
||||
'<available-deferred-tools>\nmcp__wechat__send_message\n</available-deferred-tools>',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('preserves roles for OpenAI-style array content messages', async () => {
|
||||
const { convertMessagesToLangfuse } = await import('../convert.js')
|
||||
const result = convertMessagesToLangfuse([
|
||||
{
|
||||
role: 'system',
|
||||
content: [{ type: 'text', text: 'system reminder' }],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_1',
|
||||
content: [{ type: 'text', text: 'tool output' }],
|
||||
},
|
||||
])
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'system', content: 'system reminder' },
|
||||
{ role: 'tool', content: 'tool output', tool_call_id: 'call_1' },
|
||||
])
|
||||
})
|
||||
|
||||
test('merges assistant tool calls from OpenAI-style array content', async () => {
|
||||
const { convertMessagesToLangfuse } = await import('../convert.js')
|
||||
const result = convertMessagesToLangfuse([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'calling a tool',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_from_part',
|
||||
type: 'function',
|
||||
function: { name: 'part_tool', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_from_message',
|
||||
type: 'function',
|
||||
function: { name: 'message_tool', arguments: '{"ok":true}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'calling a tool',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_from_message',
|
||||
type: 'function',
|
||||
function: { name: 'message_tool', arguments: '{"ok":true}' },
|
||||
},
|
||||
{
|
||||
id: 'call_from_part',
|
||||
type: 'function',
|
||||
function: { name: 'part_tool', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// ── client tests ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isLangfuseEnabled', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - tool_result blocks → separate { role: 'tool' } messages
|
||||
*/
|
||||
|
||||
import type { Message, AssistantMessage, UserMessage } from 'src/types/message.js'
|
||||
import type { AssistantMessage } from 'src/types/message.js'
|
||||
|
||||
type LangfuseContentPart =
|
||||
| { type: 'text'; text: string }
|
||||
@@ -30,6 +30,55 @@ type LangfuseChatMessage = {
|
||||
tool_call_id?: string
|
||||
}
|
||||
|
||||
function isLangfuseRole(value: unknown): value is LangfuseChatMessage['role'] {
|
||||
switch (value) {
|
||||
case 'user':
|
||||
case 'assistant':
|
||||
case 'system':
|
||||
case 'tool':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isLangfuseToolCall(value: unknown): value is LangfuseToolCall {
|
||||
if (!isRecord(value)) return false
|
||||
const fn = value.function
|
||||
return (
|
||||
typeof value.id === 'string' &&
|
||||
value.type === 'function' &&
|
||||
isRecord(fn) &&
|
||||
typeof fn.name === 'string' &&
|
||||
typeof fn.arguments === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
function getToolCalls(value: unknown): LangfuseToolCall[] {
|
||||
return Array.isArray(value) ? value.filter(isLangfuseToolCall) : []
|
||||
}
|
||||
|
||||
function getContentToolCalls(content: unknown[]): LangfuseToolCall[] {
|
||||
return content.flatMap(block =>
|
||||
isRecord(block) ? getToolCalls(block.tool_calls) : [],
|
||||
)
|
||||
}
|
||||
|
||||
function mergeToolCalls(
|
||||
...groups: readonly LangfuseToolCall[][]
|
||||
): LangfuseToolCall[] {
|
||||
const merged = new Map<string, LangfuseToolCall>()
|
||||
for (const toolCall of groups.flat()) {
|
||||
const key = toolCall.id || `${toolCall.function.name}:${toolCall.function.arguments}`
|
||||
if (!merged.has(key)) merged.set(key, toolCall)
|
||||
}
|
||||
return [...merged.values()]
|
||||
}
|
||||
|
||||
/** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */
|
||||
function toContentPart(block: Record<string, unknown>): LangfuseContentPart | null {
|
||||
const type = block.type as string | undefined
|
||||
@@ -121,15 +170,15 @@ function collapseContent(parts: LangfuseContentPart[]): string | LangfuseContent
|
||||
return parts
|
||||
}
|
||||
|
||||
function toRole(msg: Message): 'user' | 'assistant' | 'system' {
|
||||
function toRoleFromWrappedMessage(msg: Record<string, unknown>): 'user' | 'assistant' | 'system' {
|
||||
if (msg.type === 'assistant') return 'assistant'
|
||||
if (msg.type === 'system') return 'system'
|
||||
return 'user'
|
||||
}
|
||||
|
||||
/** Convert messagesForAPI (UserMessage | AssistantMessage)[] → Langfuse input format */
|
||||
/** Convert internal or OpenAI-style messages → Langfuse input format */
|
||||
export function convertMessagesToLangfuse(
|
||||
messages: (UserMessage | AssistantMessage)[],
|
||||
messages: readonly unknown[],
|
||||
systemPrompt?: readonly string[],
|
||||
): LangfuseChatMessage[] {
|
||||
const result: LangfuseChatMessage[] = []
|
||||
@@ -139,18 +188,34 @@ export function convertMessagesToLangfuse(
|
||||
}
|
||||
}
|
||||
for (const msg of messages) {
|
||||
const inner = msg.message
|
||||
if (!inner) continue
|
||||
const role = (inner.role as 'user' | 'assistant' | undefined) ?? toRole(msg)
|
||||
if (!isRecord(msg)) continue
|
||||
const wrappedMessage = msg.message
|
||||
const isWrappedMessage = isRecord(wrappedMessage)
|
||||
const inner = isWrappedMessage ? wrappedMessage : msg
|
||||
const role =
|
||||
isLangfuseRole(inner.role) ? inner.role : isWrappedMessage ? toRoleFromWrappedMessage(msg) : 'user'
|
||||
const rawContent = inner.content
|
||||
if (typeof rawContent === 'string' || !Array.isArray(rawContent)) {
|
||||
result.push({ role, content: String(rawContent ?? '') })
|
||||
const toolCalls = getToolCalls(inner.tool_calls)
|
||||
result.push({
|
||||
role,
|
||||
content: String(rawContent ?? ''),
|
||||
...('tool_call_id' in inner && typeof inner.tool_call_id === 'string'
|
||||
? { tool_call_id: inner.tool_call_id }
|
||||
: {}),
|
||||
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
// Extract tool_use → tool_calls at message level
|
||||
const { tool_calls, rest } = extractToolCalls(rawContent)
|
||||
const allToolCalls = mergeToolCalls(
|
||||
tool_calls,
|
||||
getToolCalls(inner.tool_calls),
|
||||
getContentToolCalls(rest),
|
||||
)
|
||||
const parts = rest
|
||||
.filter((b): b is Record<string, unknown> => b != null && typeof b === 'object')
|
||||
.map(b => toContentPart(b))
|
||||
@@ -158,7 +223,7 @@ export function convertMessagesToLangfuse(
|
||||
result.push({
|
||||
role: 'assistant',
|
||||
content: collapseContent(parts),
|
||||
...(tool_calls.length > 0 && { tool_calls }),
|
||||
...(allToolCalls.length > 0 && { tool_calls: allToolCalls }),
|
||||
})
|
||||
} else {
|
||||
// User messages: extract tool_result → separate tool messages
|
||||
@@ -168,7 +233,18 @@ export function convertMessagesToLangfuse(
|
||||
.map(b => toContentPart(b))
|
||||
.filter((p): p is LangfuseContentPart => p !== null)
|
||||
if (parts.length > 0 || toolMessages.length === 0) {
|
||||
result.push({ role: 'user', content: collapseContent(parts) })
|
||||
const toolCalls = mergeToolCalls(
|
||||
getToolCalls(inner.tool_calls),
|
||||
getContentToolCalls(rest),
|
||||
)
|
||||
result.push({
|
||||
role,
|
||||
content: collapseContent(parts),
|
||||
...('tool_call_id' in inner && typeof inner.tool_call_id === 'string'
|
||||
? { tool_call_id: inner.tool_call_id }
|
||||
: {}),
|
||||
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
||||
})
|
||||
}
|
||||
result.push(...toolMessages)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user