Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 94 additions & 20 deletions src/services/api/openai/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
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 { getOpenAIClient } from './client.js'
import { anthropicMessagesToOpenAI } from './convertMessages.js'
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './convertTools.js'
import {
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
} from './convertTools.js'
import { adaptOpenAIStreamToAnthropic } from './streamAdapter.js'
import { resolveOpenAIModel } from './modelMapping.js'
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
import { toolToAPISchema } from '../../../utils/api.js'
import { getEmptyToolPermissionContext } from '../../../Tool.js'
import {
getEmptyToolPermissionContext,
toolMatchesName,
} from '../../../Tool.js'
import { logForDebugging } from '../../../utils/debug.js'
import { addToTotalSessionCost } from '../../../cost-tracker.js'
import { calculateUSDCost } from '../../../utils/modelCost.js'
Expand All @@ -19,6 +30,14 @@ import {
createAssistantAPIErrorMessage,
normalizeContentFromAPI,
} from '../../../utils/messages.js'
import {
isToolSearchEnabled,
extractDiscoveredToolNames,
} from '../../../utils/toolSearch.js'
import {
isDeferredTool,
TOOL_SEARCH_TOOL_NAME,
} from '../../../tools/ToolSearchTool/prompt.js'

/**
* OpenAI-compatible query path. Converts Anthropic-format messages/tools to
Expand All @@ -43,41 +62,97 @@ export async function* queryModelOpenAI(
// 2. Normalize messages using shared preprocessing
const messagesForAPI = normalizeMessagesForAPI(messages, tools)

// 3. Build tool schemas
// 3. Check if tool search is enabled (similar to Anthropic path)
const useToolSearch = await isToolSearchEnabled(
options.model,
tools,
options.getToolPermissionContext ||
(async () => getEmptyToolPermissionContext()),
options.agents || [],
options.querySource,
)

// 4. Build deferred tools set (similar to Anthropic path)
const deferredToolNames = new Set<string>()
if (useToolSearch) {
for (const t of tools) {
if (isDeferredTool(t)) deferredToolNames.add(t.name)
}
}

// 5. Filter tools (similar to Anthropic path)
let filteredTools = tools
if (useToolSearch && deferredToolNames.size > 0) {
const discoveredToolNames = extractDiscoveredToolNames(messages)

filteredTools = tools.filter(tool => {
// Always include non-deferred tools
if (!deferredToolNames.has(tool.name)) return true
// Always include ToolSearchTool (so it can discover more tools)
if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
// Only include deferred tools that have been discovered
return discoveredToolNames.has(tool.name)
})
}

// 6. Build tool schemas with deferLoading flag
const toolSchemas = await Promise.all(
tools.map(tool =>
filteredTools.map(tool =>
toolToAPISchema(tool, {
getToolPermissionContext: options.getToolPermissionContext,
tools,
agents: options.agents,
allowedAgentTypes: options.allowedAgentTypes,
model: options.model,
deferLoading: useToolSearch && deferredToolNames.has(tool.name),
}),
),
)
// Filter out non-standard tools (server tools like advisor)

// 7. Filter out non-standard tools (server tools like advisor)
const standardTools = toolSchemas.filter(
(t): t is BetaToolUnion & { type: string } => {
const anyT = t as Record<string, unknown>
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
return (
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
)
},
)

// 4. Convert messages and tools to OpenAI format
const openaiMessages = anthropicMessagesToOpenAI(messagesForAPI, systemPrompt)
// 8. Convert messages and tools to OpenAI format
const openaiMessages = anthropicMessagesToOpenAI(
messagesForAPI,
systemPrompt,
)
const openaiTools = anthropicToolsToOpenAI(standardTools)
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)

// 5. Get client and make streaming request
// 9. Log tool filtering details
if (useToolSearch) {
const includedDeferredTools = filteredTools.filter(t =>
deferredToolNames.has(t.name),
).length
logForDebugging(
`[OpenAI] Tool search enabled: ${includedDeferredTools}/${deferredToolNames.size} deferred tools included, total tools=${openaiTools.length}`,
)
} else {
logForDebugging(
`[OpenAI] Tool search disabled, total tools=${openaiTools.length}`,
)
}

// 10. Get client and make streaming request
const client = getOpenAIClient({
maxRetries: 0,
fetchOverride: options.fetchOverride,
source: options.querySource,
})

logForDebugging(`[OpenAI] Calling model=${openaiModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`)
logForDebugging(
`[OpenAI] Calling model=${openaiModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
)

// 6. Call OpenAI API with streaming
// 11. Call OpenAI API with streaming
const stream = await client.chat.completions.create(
{
model: openaiModel,
Expand All @@ -103,7 +178,7 @@ export async function* queryModelOpenAI(

// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
const contentBlocks: Record<number, any> = {}
let partialMessage: any = undefined
let partialMessage: any
let usage = {
input_tokens: 0,
output_tokens: 0,
Expand All @@ -121,7 +196,7 @@ export async function* queryModelOpenAI(
if ((event as any).message?.usage) {
usage = {
...usage,
...((event as any).message.usage),
...(event as any).message.usage,
}
}
break
Expand Down Expand Up @@ -164,11 +239,7 @@ export async function* queryModelOpenAI(
const m: AssistantMessage = {
message: {
...partialMessage,
content: normalizeContentFromAPI(
[block],
tools,
options.agentId,
),
content: normalizeContentFromAPI([block], tools, options.agentId),
},
requestId: undefined,
type: 'assistant',
Expand All @@ -192,7 +263,10 @@ export async function* queryModelOpenAI(
}

// Track cost and token usage (matching the Anthropic path in claude.ts)
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(openaiModel, usage as any)
addToTotalSessionCost(costUSD, usage as any, options.model)
}
Expand Down
Loading