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:
parent
b00608c2f3
commit
c65bbeaeba
|
|
@ -1,5 +1,6 @@
|
||||||
import type Anthropic from '@anthropic-ai/sdk'
|
import type Anthropic from '@anthropic-ai/sdk'
|
||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages.js'
|
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages.js'
|
||||||
|
import OpenAI from 'openai'
|
||||||
import {
|
import {
|
||||||
getLastApiCompletionTimestamp,
|
getLastApiCompletionTimestamp,
|
||||||
setLastApiCompletionTimestamp,
|
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 type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/metadata.js'
|
||||||
import { getAPIMetadata } from '../services/api/claude.js'
|
import { getAPIMetadata } from '../services/api/claude.js'
|
||||||
import { getAnthropicClient } from '../services/api/client.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 { getModelBetas, modelSupportsStructuredOutputs } from './betas.js'
|
||||||
import { computeFingerprint } from './fingerprint.js'
|
import { computeFingerprint } from './fingerprint.js'
|
||||||
import { normalizeModelStringForAPI } from './model/model.js'
|
import { normalizeModelStringForAPI } from './model/model.js'
|
||||||
|
import { getAPIProvider } from './model/providers.js'
|
||||||
|
|
||||||
type MessageParam = Anthropic.MessageParam
|
type MessageParam = Anthropic.MessageParam
|
||||||
type TextBlockParam = Anthropic.TextBlockParam
|
type TextBlockParam = Anthropic.TextBlockParam
|
||||||
|
|
@ -121,6 +131,15 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
||||||
stop_sequences,
|
stop_sequences,
|
||||||
} = opts
|
} = opts
|
||||||
|
|
||||||
|
// Route to appropriate provider
|
||||||
|
const provider = getAPIProvider()
|
||||||
|
if (provider === 'costrict') {
|
||||||
|
return sideQueryCoStrict(opts)
|
||||||
|
}
|
||||||
|
if (provider === 'openai') {
|
||||||
|
return sideQueryOpenAI(opts)
|
||||||
|
}
|
||||||
|
|
||||||
const client = await getAnthropicClient({
|
const client = await getAnthropicClient({
|
||||||
maxRetries,
|
maxRetries,
|
||||||
model,
|
model,
|
||||||
|
|
@ -220,3 +239,207 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
||||||
|
|
||||||
return response
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user