forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcreate-messages-native.ts
More file actions
167 lines (146 loc) · 5.41 KB
/
create-messages-native.ts
File metadata and controls
167 lines (146 loc) · 5.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* Native Anthropic pass-through service.
*
* The GitHub Copilot upstream (`api.enterprise.githubcopilot.com`) natively
* speaks the Anthropic Messages API for all Claude 4.5+ models. Routing
* requests directly to `/v1/messages` instead of translating them through
* `/chat/completions` gives us:
*
* - Real thinking blocks with `signature` field (multi-turn reasoning)
* - `cache_creation_input_tokens` in usage
* - `top_k` support
* - No lossy translation round-trip
*
* See research notes: ~/copilot-models-litellm/copilot_models.py
*/
import consola from "consola"
import { events } from "fetch-event-stream"
import type { AnthropicMessagesPayload } from "~/routes/messages/anthropic-types"
import { copilotBaseUrl, copilotHeaders } from "~/lib/api-config"
import { HTTPError } from "~/lib/error"
import { state } from "~/lib/state"
/**
* Forward an Anthropic-format request directly to Copilot's native `/v1/messages`
* endpoint, preserving all fields (thinking, signature, top_k, cache_control, …).
*
* Returns:
* - For non-streaming: the raw Anthropic JSON response object
* - For streaming: an async iterable of SSE events (fetch-event-stream)
*/
export const createMessagesNative = async (
payload: AnthropicMessagesPayload,
) => {
if (!state.copilotToken) throw new Error("Copilot token not found")
const hasVision = messageHasImages(payload)
const headers = buildNativeHeaders(hasVision, Boolean(payload.stream))
const upstream = `${copilotBaseUrl(state)}/v1/messages`
consola.debug("Native Anthropic upstream:", upstream)
// Strip fields that are Copilot-API–specific or unsupported by upstream
const body = buildUpstreamPayload(payload)
const response = await fetch(upstream, {
method: "POST",
headers,
body: JSON.stringify(body),
})
if (!response.ok) {
consola.error("Native Anthropic upstream error", response.status)
throw new HTTPError("Native Anthropic upstream error", response)
}
if (payload.stream) {
return events(response)
}
return response.json()
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Build headers for the Anthropic native endpoint.
*
* The upstream requires `anthropic-version` and does NOT want an `openai-intent`
* header. We reuse `copilotHeaders()` for auth/agent headers and then layer the
* Anthropic-specific ones on top.
*/
function buildNativeHeaders(
vision: boolean,
stream: boolean,
): Record<string, string> {
const base = copilotHeaders(state, vision)
// Remove headers that are OpenAI-specific and not expected by Anthropic endpoint
const { "openai-intent": _dropped, ...anthropicBase } = base
return {
...anthropicBase,
"anthropic-version": "2023-06-01",
// Enable beta features: extended thinking + prompt caching
"anthropic-beta":
"interleaved-thinking-2025-05-14,prompt-caching-2024-07-31",
// Only request SSE streaming format when the caller is streaming
...(stream ? { accept: "text/event-stream" } : {}),
}
}
/**
* Produce the payload forwarded to upstream.
*
* We pass through almost everything verbatim. The only transformation is that
* `claude-opus-4.7+` requires the new adaptive thinking format
* (`thinking: { type: "adaptive" }` + `output_config.effort`) rather than the
* legacy `{ type: "enabled", budget_tokens: N }`. If the caller already sent
* the correct format we leave it alone; if they sent the old format and the
* model requires adaptive, we upgrade automatically.
*/
export function buildUpstreamPayload(
payload: AnthropicMessagesPayload,
): AnthropicMessagesPayload {
const { thinking, output_config, ...rest } = payload
if (!thinking) {
return rest // safe: output_config only valid alongside thinking
}
if (isAdaptiveThinkingModel(payload.model)) {
// Upgrade legacy enabled → adaptive if needed
if (thinking.type === "enabled") {
consola.debug(
`Upgrading thinking format to adaptive for model ${payload.model}`,
)
return {
...rest,
thinking: { type: "adaptive" },
output_config:
output_config?.effort ? output_config : { effort: "medium" },
}
}
// Already adaptive — forward as-is
return { ...rest, thinking, output_config }
}
// Non-adaptive model — forward legacy format, drop output_config
return { ...rest, thinking }
}
/**
* Returns true for models that require the adaptive thinking API
* (`{ type: "adaptive" }` + `output_config.effort`) rather than the
* legacy `{ type: "enabled", budget_tokens: N }`.
* Currently: claude-opus-4.7 and later.
*/
function isAdaptiveThinkingModel(model: string): boolean {
// claude-opus-4.7 and above use adaptive thinking
const match = model.match(/^claude-opus-4[.-](\d+)/)
if (match) {
const minor = Number.parseInt(match[1], 10)
// claude-opus-4.7 and later use the new adaptive thinking API (not legacy budget_tokens)
return minor >= 7
}
return false
}
/**
* Check whether the request contains any image blocks (to set vision headers).
*/
function messageHasImages(payload: AnthropicMessagesPayload): boolean {
for (const msg of payload.messages) {
if (typeof msg.content === "string") continue
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "image") return true
}
}
}
return false
}