claude-code-best/src/utils/sideQueryOpenAICompat.ts
Askhz 2f7fc7959d fix: 修复 sideQuery/模型选择在 CoStrict 和 OpenAI provider 下的问题
- sideQuery:新增 openai/costrict provider 分支,分别走各自的 base_url
  而非 Anthropic API;提取 sideQueryOpenAICompat 共用转换逻辑;
  CoStrict 分支统一使用主循环模型(getMainLoopModel)
- ConsoleOAuthFlow:CoStrict 登录选模型后同步写入 settings.json
- /model 命令:CoStrict provider 下模型切换持久化到 settings.json;
  支持 costrict-login 触发登录并重载模型列表;未登录时提示先 /login
- modelOptions:CoStrict provider 展示服务器动态模型列表;
  仅对 Anthropic 兼容 provider 注入自定义模型到选项列表
- configs:补充 CoStrict 的 claude-opus-4-7 模型配置
- cli:默认禁用非必要 Anthropic 流量(CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:02:53 +08:00

133 lines
4.2 KiB
TypeScript

/**
* OpenAI-compatible sideQuery for providers that use the Chat Completions API
* (openai, grok, costrict). Callers pass a pre-configured OpenAI client and a
* resolved model name; the rest of the conversion is shared.
*/
import type OpenAI from 'openai'
import type { BetaMessage } from '@anthropic-ai/sdk/resources/beta/messages.js'
import type { SideQueryOptions } from './sideQuery.js'
import { logEvent } from '../services/analytics/index.js'
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../services/analytics/metadata.js'
import {
getLastApiCompletionTimestamp,
setLastApiCompletionTimestamp,
} from '../bootstrap/state.js'
import { logForDebugging } from './debug.js'
function buildMessages(
system: SideQueryOptions['system'],
messages: SideQueryOptions['messages'],
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
const result: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = []
if (system) {
const systemText = Array.isArray(system)
? system.map(b => b.text).join('\n\n')
: system
if (systemText.trim()) {
result.push({ role: 'system', content: systemText })
}
}
for (const msg of messages) {
if (typeof msg.content === 'string') {
result.push({ role: msg.role as 'user' | 'assistant', content: msg.content })
} else {
const text = msg.content
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
.map(b => b.text)
.join('\n')
result.push({ role: msg.role as 'user' | 'assistant', content: text })
}
}
return result
}
const STOP_REASON_MAP: Record<string, BetaMessage['stop_reason']> = {
stop: 'end_turn',
length: 'max_tokens',
tool_calls: 'tool_use',
content_filter: 'end_turn',
}
export async function sideQueryOpenAICompat(
opts: SideQueryOptions,
client: OpenAI,
resolvedModel: string,
providerTag: string,
): Promise<BetaMessage> {
const { system, messages, max_tokens = 1024, signal, temperature, stop_sequences, querySource } = opts
logForDebugging(`[${providerTag} sideQuery] querySource=${querySource}, model=${resolvedModel}`)
const start = Date.now()
const response = await client.chat.completions.create(
{
model: resolvedModel,
messages: buildMessages(system, messages),
max_tokens,
stream: false,
...(temperature !== undefined && { temperature }),
...(stop_sequences && { stop: stop_sequences }),
},
{ signal },
)
const now = Date.now()
const lastCompletion = getLastApiCompletionTimestamp()
const inputTokens = response.usage?.prompt_tokens ?? 0
const outputTokens = response.usage?.completion_tokens ?? 0
logEvent('tengu_api_success', {
requestId: (response.id ?? '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
querySource: querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
model: resolvedModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
inputTokens,
outputTokens,
cachedInputTokens: 0,
uncachedInputTokens: 0,
durationMsIncludingRetries: now - start,
timeSinceLastApiCallMs: lastCompletion !== null ? now - lastCompletion : undefined,
})
setLastApiCompletionTimestamp(now)
const choice = response.choices[0]
const content: BetaMessage['content'] = []
if (choice?.message?.content) {
content.push({ type: 'text', text: choice.message.content })
}
if (choice?.message?.tool_calls) {
for (const tc of choice.message.tool_calls) {
let input: Record<string, unknown> = {}
try {
input = JSON.parse(tc.function.arguments) as Record<string, unknown>
} catch {
// leave input empty on parse failure
}
content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input })
}
}
return {
id: response.id,
type: 'message',
role: 'assistant',
content,
model: resolvedModel,
stop_reason: STOP_REASON_MAP[choice?.finish_reason ?? 'stop'] ?? 'end_turn',
stop_sequence: null,
usage: {
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: null,
cache_read_input_tokens: null,
server_tool_use: null,
},
} as unknown as BetaMessage
}