claude-code-best/src/costrict/provider/index.ts
Askhz 1970c698ea feat(costrict): 根据模型列表动态设置 max_tokens 参数
从 /ai-gateway/api/v1/models 返回的模型信息中读取 maxTokensKey
和 maxTokens 字段,发送请求时动态注入对应参数名和参数值。
maxTokensKey 缺失时默认使用 max_tokens,maxTokens 无值时不传该参数。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:23:28 +08:00

283 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* CoStrict 查询入口
* 复用 OpenAI 兼容路径,注入 CoStrict 自定义 fetch 和 baseURL
*/
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { SDKAssistantMessageError } from '../../entrypoints/agentSdkTypes.js'
import type { SystemPrompt } from '../../utils/systemPromptType.js'
import type {
Message,
StreamEvent,
SystemAPIErrorMessage,
AssistantMessage,
} from '../../types/message.js'
import type { Tools } from '../../Tool.js'
import type { Options } from '../../services/api/claude.js'
import OpenAI from 'openai'
import { getProxyFetchOptions } from '../../utils/proxy.js'
import { getUserAgent } from '../../utils/http.js'
import { anthropicMessagesToOpenAI, anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI, adaptOpenAIStreamToAnthropic } from '@ant/model-provider'
import { normalizeMessagesForAPI } from '../../utils/messages.js'
import { toolToAPISchema } from '../../utils/api.js'
import { logForDebugging } from '../../utils/debug.js'
import { addToTotalSessionCost } from '../../cost-tracker.js'
import { calculateUSDCost } from '../../utils/modelCost.js'
import {
createAssistantAPIErrorMessage,
normalizeContentFromAPI,
} from '../../utils/messages.js'
import { randomUUID } from 'crypto'
import { createCoStrictFetch } from './fetch.js'
import { resolveCoStrictModel } from './modelMapping.js'
import { getCoStrictBaseURL } from './auth.js'
import { loadCoStrictCredentials } from './credentials.js'
import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js'
import { fetchCoStrictModels } from './models.js'
/**
* CoStrict 查询路径
* 与 queryModelOpenAI 结构相同,使用 CoStrict 自定义 fetch 和 baseURL
*/
export async function* queryModelCoStrict(
messages: Message[],
systemPrompt: SystemPrompt,
tools: Tools,
signal: AbortSignal,
options: Options,
): AsyncGenerator<
StreamEvent | AssistantMessage | SystemAPIErrorMessage,
void
> {
try {
// 1. 解析模型名
const costrictModel = resolveCoStrictModel(options.model)
// 2. 获取 CoStrict base URL
const creds = await loadCoStrictCredentials()
const baseUrl = getCoStrictBaseURL(creds?.base_url)
const chatBaseURL = `${baseUrl}/chat-rag/api/v1`
// 3. 从模型列表获取 maxTokens 相关参数
let maxTokensParamKey: string = 'max_tokens'
let maxTokensValue: number | undefined
if (creds?.access_token) {
try {
const modelList = await fetchCoStrictModels(baseUrl, creds.access_token)
const modelInfo = modelList.find(m => m.id === costrictModel)
if (modelInfo) {
maxTokensParamKey = modelInfo.maxTokensKey || 'max_tokens'
if (modelInfo.maxTokens != null) {
maxTokensValue = modelInfo.maxTokens
}
}
} catch {
// 获取模型列表失败,使用默认值
}
}
// 4. 规范化消息
const messagesForAPI = normalizeMessagesForAPI(messages, tools)
// 5. 构建工具 schema
const toolSchemas = await Promise.all(
tools.map(tool =>
toolToAPISchema(tool, {
getToolPermissionContext: options.getToolPermissionContext,
tools,
agents: options.agents,
allowedAgentTypes: options.allowedAgentTypes,
model: options.model,
}),
),
)
const standardTools = toolSchemas.filter(
(t): t is BetaToolUnion & { type: string } => {
const anyT = t as unknown as Record<string, unknown>
return (
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
)
},
)
// 6. 转换为 OpenAI 格式
// 根据模型名称自动检测是否启用thinking模式
const enableThinking = isOpenAIThinkingEnabled(costrictModel)
const openaiMessages = anthropicMessagesToOpenAI(
messagesForAPI,
systemPrompt,
{ enableThinking }
)
const openaiTools = anthropicToolsToOpenAI(standardTools)
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
// 7. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch
const costrictFetch = createCoStrictFetch()
const client = new OpenAI({
apiKey: 'costrict-managed', // 实际 token 由 createCoStrictFetch 注入
baseURL: chatBaseURL,
maxRetries: 0,
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10),
dangerouslyAllowBrowser: true,
fetchOptions: getProxyFetchOptions({
forAnthropicAPI: false,
}) as any,
fetch: costrictFetch as any,
defaultHeaders: { 'User-Agent': getUserAgent() },
})
logForDebugging(
`[CoStrict] model=${costrictModel}, baseURL=${chatBaseURL}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
)
// 8. 调用 API流式
const requestBody: Record<string, unknown> = {
model: costrictModel,
messages: openaiMessages,
...(openaiTools.length > 0 && {
tools: openaiTools,
...(openaiToolChoice && {
tool_choice:
openaiToolChoice as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption,
}),
}),
stream: true,
stream_options: { include_usage: true },
...(options.temperatureOverride !== undefined && {
temperature: options.temperatureOverride,
}),
...(maxTokensValue != null && {
[maxTokensParamKey]: maxTokensValue,
}),
}
const stream = await client.chat.completions.create(
requestBody as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,
{ signal },
)
// 9. 转换流并 yield 事件
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel)
const contentBlocks: Record<number, any> = {}
// 跟踪已 yield 的 assistant messages用于 message_delta 时回写 usage
const yieldedMessages: AssistantMessage[] = []
let partialMessage: any
let stopReason: string | null = null
let usage = {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
}
let ttftMs = 0
const start = Date.now()
for await (const event of adaptedStream) {
switch (event.type) {
case 'message_start': {
partialMessage = (event as any).message
ttftMs = Date.now() - start
if ((event as any).message?.usage) {
usage = { ...usage, ...(event as any).message.usage }
}
break
}
case 'content_block_start': {
const idx = (event as any).index
const cb = (event as any).content_block
if (cb.type === 'tool_use') {
contentBlocks[idx] = { ...cb, input: '' }
} else if (cb.type === 'text') {
contentBlocks[idx] = { ...cb, text: '' }
} else if (cb.type === 'thinking') {
contentBlocks[idx] = { ...cb, thinking: '', signature: '' }
} else {
contentBlocks[idx] = { ...cb }
}
break
}
case 'content_block_delta': {
const idx = (event as any).index
const delta = (event as any).delta
const block = contentBlocks[idx]
if (!block) break
if (delta.type === 'text_delta') {
block.text = (block.text || '') + delta.text
} else if (delta.type === 'input_json_delta') {
block.input = (block.input || '') + delta.partial_json
} else if (delta.type === 'thinking_delta') {
block.thinking = (block.thinking || '') + delta.thinking
} else if (delta.type === 'signature_delta') {
block.signature = delta.signature
}
break
}
case 'content_block_stop': {
const idx = (event as any).index
const block = contentBlocks[idx]
if (!block || !partialMessage) break
const m: AssistantMessage = {
message: {
...partialMessage,
content: normalizeContentFromAPI([block], tools, options.agentId),
usage,
},
requestId: undefined,
type: 'assistant',
uuid: randomUUID(),
timestamp: new Date().toISOString(),
}
yieldedMessages.push(m)
yield m
break
}
case 'message_delta': {
const deltaUsage = (event as any).usage
if (deltaUsage) usage = { ...usage, ...deltaUsage }
// 回写 usage 到已 yield 的 assistant messages
// 与 Anthropic 原生路径 claude.ts:2298 保持一致
for (const msg of yieldedMessages) {
msg.message.usage = usage
}
// 记录 stop_reason回写到最后的 message
if ((event as any).delta?.stop_reason != null) {
stopReason = (event as any).delta.stop_reason
const lastMsg = yieldedMessages[yieldedMessages.length - 1]
if (lastMsg) {
lastMsg.message.stop_reason = stopReason
}
}
break
}
case 'message_stop':
break
}
if (
event.type === 'message_stop' &&
usage.input_tokens + usage.output_tokens > 0
) {
const costUSD = calculateUSDCost(costrictModel, usage as any)
addToTotalSessionCost(costUSD, usage as any, options.model)
}
yield {
type: 'stream_event',
event,
...(event.type === 'message_start' ? { ttftMs } : undefined),
} as StreamEvent
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
logForDebugging(`[CoStrict] Error: ${errorMsg}`, { level: 'error' })
yield createAssistantAPIErrorMessage({
content: `CoStrict API Error: ${errorMsg}`,
apiError: 'api_error',
error:
error instanceof Error
? (error as unknown as SDKAssistantMessageError)
: undefined,
})
}
}