copilot-api / src /routes /messages /handler.ts
imspsycho's picture
Initial upload from Google Colab
98c9143 verified
Raw History Blame Contribute Delete
5.69 kB
import type { Context } from "hono"
import type { Model } from "~/services/copilot/get-models"
import { awaitApproval } from "~/lib/approval"
import { COMPACT_REQUEST } from "~/lib/compact"
import {
getSmallModel,
isMessagesApiEnabled,
resolveMappedModel,
} from "~/lib/config"
import { createHandlerLogger, debugJson } from "~/lib/logger"
import { findEndpointModel } from "~/lib/models"
import { parseProviderModelAlias } from "~/lib/provider-model"
import { checkRateLimit } from "~/lib/rate-limit"
import { state } from "~/lib/state"
import { generateRequestIdFromPayload, getRootSessionId } from "~/lib/utils"
import { handleProviderMessagesForProvider } from "~/routes/provider/messages/handler"
import { getResponsesTransportForModel } from "~/routes/responses/utils"
import type { AnthropicMessagesPayload } from "./anthropic-types"
import {
handleWithChatCompletions,
handleWithMessagesApi,
handleWithResponsesApi,
} from "./api-flows"
import {
applyLastMessageCacheControl,
getCompactType,
getLastMessageContentCacheControl,
mergeToolResultForClaude,
sanitizeIdeTools,
stripToolReferenceTurnBoundary,
} from "./preprocess"
import { parseSubagentMarkerFromFirstUser } from "./subagent-marker"
const logger = createHandlerLogger("messages-handler")
export const messagesFlowHandlers = {
handleWithChatCompletions,
handleWithMessagesApi,
handleWithResponsesApi,
}
export async function handleCompletion(c: Context) {
const anthropicPayload = await c.req.json<AnthropicMessagesPayload>()
const requestedModel = anthropicPayload.model
anthropicPayload.model = resolveMappedModel(anthropicPayload.model)
if (anthropicPayload.model !== requestedModel) {
logger.debug(
`Resolved model mapping: ${requestedModel} -> ${anthropicPayload.model}`,
)
}
const providerModelAlias = parseProviderModelAlias(anthropicPayload.model)
if (providerModelAlias) {
anthropicPayload.model = providerModelAlias.model
return await handleProviderMessagesForProvider(c, {
payload: anthropicPayload,
provider: providerModelAlias.provider,
})
}
await checkRateLimit(state)
debugJson(logger, "Anthropic request payload:", anthropicPayload)
sanitizeIdeTools(anthropicPayload)
const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload)
if (subagentMarker) {
debugJson(logger, "Detected Subagent marker:", subagentMarker)
}
const sessionId = getRootSessionId(anthropicPayload, c)
logger.debug("Extracted session ID:", sessionId)
// claude code and opencode compact / auto-continue detection
const compactType = getCompactType(anthropicPayload)
// fix claude code 2.0.28+ warmup request consume premium request, forcing small model if no tools are used
// set "CLAUDE_CODE_SUBAGENT_MODEL": "you small model" also can avoid this
const anthropicBeta = c.req.header("anthropic-beta")
logger.debug("Anthropic Beta header:", anthropicBeta)
const noTools = !anthropicPayload.tools || anthropicPayload.tools.length === 0
if (anthropicBeta && noTools && compactType === 0) {
anthropicPayload.model = getSmallModel()
}
if (compactType) {
logger.debug("Compact request type:", compactType)
}
const lastMessageCacheControl = getLastMessageContentCacheControl(
anthropicPayload.messages.at(-1),
)
stripToolReferenceTurnBoundary(anthropicPayload)
// Merge tool_result and text blocks into tool_result to avoid consuming premium requests
// (caused by skill invocations, edit hooks, plan or to do reminders)
// e.g. {"role":"user","content":[{"type":"tool_result","content":"Launching skill: xxx"},{"type":"text","text":"xxx"}]}
// not only for claude, but also for opencode
// compact requests still run this processing, except for the final compact message itself
mergeToolResultForClaude(anthropicPayload, {
skipLastMessage: compactType === COMPACT_REQUEST,
})
applyLastMessageCacheControl(anthropicPayload, lastMessageCacheControl)
const requestId = generateRequestIdFromPayload(anthropicPayload, sessionId)
logger.debug("Generated request ID:", requestId)
if (state.manualApprove) {
await awaitApproval()
}
const selectedModel = findEndpointModel(anthropicPayload.model)
anthropicPayload.model = selectedModel?.id ?? anthropicPayload.model
if (shouldUseMessagesApi(selectedModel)) {
return await messagesFlowHandlers.handleWithMessagesApi(
c,
anthropicPayload,
{
anthropicBetaHeader: anthropicBeta,
subagentMarker,
selectedModel,
requestId,
sessionId,
compactType,
logger,
},
)
}
if (shouldUseResponsesApi(selectedModel, compactType)) {
return await messagesFlowHandlers.handleWithResponsesApi(
c,
anthropicPayload,
{
subagentMarker,
selectedModel,
requestId,
sessionId,
compactType,
logger,
},
)
}
return await messagesFlowHandlers.handleWithChatCompletions(
c,
anthropicPayload,
{
subagentMarker,
requestId,
sessionId,
compactType,
logger,
},
)
}
const MESSAGES_ENDPOINT = "/v1/messages"
const shouldUseResponsesApi = (
selectedModel: Model | undefined,
compactType: ReturnType<typeof getCompactType>,
): boolean => {
return Boolean(getResponsesTransportForModel(selectedModel, { compactType }))
}
const shouldUseMessagesApi = (selectedModel: Model | undefined): boolean => {
const useMessagesApi = isMessagesApiEnabled()
if (!useMessagesApi) {
return false
}
return (
selectedModel?.supported_endpoints?.includes(MESSAGES_ENDPOINT) ?? false
)
}