feat(sideQuery): add OpenAI and CoStrict provider support

- Route sideQuery to appropriate provider based on config
- Add sideQueryCoStrict() for CoStrict API integration
- Add sideQueryOpenAI() for OpenAI API integration
- Both implementations return BetaMessage format for compatibility
This commit is contained in:
Askhz 2026-04-10 15:44:54 +08:00
parent b00608c2f3
commit c65bbeaeba

View File

@ -1,5 +1,6 @@
import type Anthropic from '@anthropic-ai/sdk'
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages.js'
import OpenAI from 'openai'
import {
getLastApiCompletionTimestamp,
setLastApiCompletionTimestamp,
@ -14,9 +15,18 @@ import { logEvent } from '../services/analytics/index.js'
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/metadata.js'
import { getAPIMetadata } from '../services/api/claude.js'
import { getAnthropicClient } from '../services/api/client.js'
import { getOpenAIClient } from '../services/api/openai/client.js'
import { resolveOpenAIModel } from '../services/api/openai/modelMapping.js'
import { anthropicMessagesToOpenAI } from '../services/api/openai/convertMessages.js'
import { createCoStrictFetch } from '../costrict/provider/fetch.js'
import { loadCoStrictCredentials } from '../costrict/provider/credentials.js'
import { getCoStrictBaseURL } from '../costrict/provider/auth.js'
import { resolveCoStrictModel } from '../costrict/provider/modelMapping.js'
import { getProxyFetchOptions } from './proxy.js'
import { getModelBetas, modelSupportsStructuredOutputs } from './betas.js'
import { computeFingerprint } from './fingerprint.js'
import { normalizeModelStringForAPI } from './model/model.js'
import { getAPIProvider } from './model/providers.js'
type MessageParam = Anthropic.MessageParam
type TextBlockParam = Anthropic.TextBlockParam
@ -121,6 +131,15 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
stop_sequences,
} = opts
// Route to appropriate provider
const provider = getAPIProvider()
if (provider === 'costrict') {
return sideQueryCoStrict(opts)
}
if (provider === 'openai') {
return sideQueryOpenAI(opts)
}
const client = await getAnthropicClient({
maxRetries,
model,
@ -220,3 +239,207 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
return response
}
/**
* CoStrict provider implementation for sideQuery
*/
async function sideQueryCoStrict(opts: SideQueryOptions): Promise<BetaMessage> {
const {
model,
system,
messages,
max_tokens = 1024,
signal,
skipSystemPromptPrefix,
temperature,
output_format,
} = opts
// Build system prompt
const systemContent = skipSystemPromptPrefix
? (typeof system === 'string' ? system : '')
: `${getCLISyspromptPrefix({ isNonInteractive: false, hasAppendSystemPrompt: false })}
${typeof system === 'string' ? system : ''}`
// Resolve model and get base URL
const costrictModel = resolveCoStrictModel(model)
const creds = await loadCoStrictCredentials()
const baseUrl = getCoStrictBaseURL(creds?.base_url)
const chatBaseURL = `${baseUrl}/chat-rag/api/v1`
// Create OpenAI client with CoStrict custom fetch
const costrictFetch = createCoStrictFetch()
const client = new OpenAI({
apiKey: 'costrict-managed',
baseURL: chatBaseURL,
maxRetries: 0,
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10),
dangerouslyAllowBrowser: true,
fetchOptions: getProxyFetchOptions({ forAnthropicAPI: false }) as RequestInit,
fetch: costrictFetch as any,
})
// Convert messages to OpenAI format
const openaiMessages = anthropicMessagesToOpenAI(
messages.map(m => ({
...m,
content: typeof m.content === 'string' ? m.content : m.content,
})),
systemContent,
{ enableThinking: false }
)
// Build request
const requestBody: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
model: costrictModel,
messages: openaiMessages,
max_tokens: max_tokens,
...(temperature !== undefined && { temperature }),
}
// Add response_format for JSON schema if specified
if (output_format?.type === 'json_schema') {
(requestBody as any).response_format = {
type: 'json_schema',
json_schema: {
name: 'response',
schema: output_format.schema,
strict: true,
},
}
}
const start = Date.now()
const response = await client.chat.completions.create(requestBody, { signal })
// Convert OpenAI response to Anthropic BetaMessage format
const choice = response.choices[0]
const content = choice?.message?.content || ''
const betaMessage: BetaMessage = {
id: response.id,
type: 'message',
role: 'assistant',
model: costrictModel,
content: [{ type: 'text', text: content }],
stop_reason: choice?.finish_reason === 'stop' ? 'end_turn' : 'max_tokens',
usage: {
input_tokens: response.usage?.prompt_tokens || 0,
output_tokens: response.usage?.completion_tokens || 0,
},
} as BetaMessage
// Log analytics
const now = Date.now()
const lastCompletion = getLastApiCompletionTimestamp()
logEvent('tengu_api_success', {
requestId: response.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
querySource: opts.querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
model: costrictModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
cachedInputTokens: 0,
uncachedInputTokens: 0,
durationMsIncludingRetries: now - start,
timeSinceLastApiCallMs: lastCompletion !== null ? now - lastCompletion : undefined,
})
setLastApiCompletionTimestamp(now)
return betaMessage
}
/**
* OpenAI provider implementation for sideQuery
*/
async function sideQueryOpenAI(opts: SideQueryOptions): Promise<BetaMessage> {
const {
model,
system,
messages,
max_tokens = 1024,
signal,
skipSystemPromptPrefix,
temperature,
output_format,
} = opts
// Build system prompt
const systemContent = skipSystemPromptPrefix
? (typeof system === 'string' ? system : '')
: `${getCLISyspromptPrefix({ isNonInteractive: false, hasAppendSystemPrompt: false })}
${typeof system === 'string' ? system : ''}`
// Resolve model
const openaiModel = resolveOpenAIModel(model)
// Get OpenAI client
const client = getOpenAIClient({ maxRetries: 0, source: 'side_query' })
// Convert messages to OpenAI format
const openaiMessages = anthropicMessagesToOpenAI(
messages.map(m => ({
...m,
content: typeof m.content === 'string' ? m.content : m.content,
})),
systemContent,
{ enableThinking: false }
)
// Build request
const requestBody: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
model: openaiModel,
messages: openaiMessages,
max_tokens: max_tokens,
...(temperature !== undefined && { temperature }),
}
// Add response_format for JSON schema if specified
if (output_format?.type === 'json_schema') {
(requestBody as any).response_format = {
type: 'json_schema',
json_schema: {
name: 'response',
schema: output_format.schema,
strict: true,
},
}
}
const start = Date.now()
const response = await client.chat.completions.create(requestBody, { signal })
// Convert OpenAI response to Anthropic BetaMessage format
const choice = response.choices[0]
const content = choice?.message?.content || ''
const betaMessage: BetaMessage = {
id: response.id,
type: 'message',
role: 'assistant',
model: openaiModel,
content: [{ type: 'text', text: content }],
stop_reason: choice?.finish_reason === 'stop' ? 'end_turn' : 'max_tokens',
usage: {
input_tokens: response.usage?.prompt_tokens || 0,
output_tokens: response.usage?.completion_tokens || 0,
},
} as BetaMessage
// Log analytics
const now = Date.now()
const lastCompletion = getLastApiCompletionTimestamp()
logEvent('tengu_api_success', {
requestId: response.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
querySource: opts.querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
model: openaiModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
cachedInputTokens: 0,
uncachedInputTokens: 0,
durationMsIncludingRetries: now - start,
timeSinceLastApiCallMs: lastCompletion !== null ? now - lastCompletion : undefined,
})
setLastApiCompletionTimestamp(now)
return betaMessage
}