Skip to content

Commit 20a3fbe

Browse files
committed
merge main into master: include thinking/effort fixes + usage viewer
main has all the same setup.sh / claude-copilot.sh / README / .gitignore / bun.lock improvements as master's aa4bdaf, plus: - adaptive→enabled thinking translation (avoids upstream 400 on opus-4.7) - opus-4.7 effort cap opened to low/medium/high/xhigh/max - /usage/view local HTML dashboard with auto-refresh
2 parents aa4bdaf + 8a6b551 commit 20a3fbe

1 file changed

Lines changed: 219 additions & 13 deletions

File tree

src/routes/messages/non-stream-translation.ts

Lines changed: 219 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import consola from "consola"
2+
3+
import { isClaudeOpus47Model, resolveModelId } from "~/lib/models"
14
import {
5+
sanitizeReasoningEffortForModel,
26
type ChatCompletionResponse,
37
type ChatCompletionsPayload,
48
type ContentPart,
@@ -40,26 +44,228 @@ export function translateToOpenAI(
4044
stream: payload.stream,
4145
temperature: payload.temperature,
4246
top_p: payload.top_p,
47+
thinking: translateThinking(payload),
48+
output_config: translateOutputConfig(payload),
49+
reasoning_effort: translateReasoningEffort(payload),
4350
user: payload.metadata?.user_id,
4451
tools: translateAnthropicToolsToOpenAI(payload.tools),
4552
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice),
4653
}
4754
}
4855

56+
function isClaudeModel(modelId: string): boolean {
57+
return modelId.startsWith("claude-")
58+
}
59+
60+
type ClaudeOpus47Effort = NonNullable<
61+
NonNullable<ChatCompletionsPayload["output_config"]>["effort"]
62+
>
63+
64+
// Per-model effort caps. Upstream rejects efforts above the model's tier.
65+
// TODO: keep in sync with the mirror in services/copilot/create-chat-completions.ts.
66+
// 2026-04 probe: upstream now accepts low/medium/high/xhigh/max for opus-4.7
67+
// (and silently ignores unknown values), so the cap is opened up. Keep the
68+
// helper in place so we can re-tighten without restructuring callers.
69+
const OPUS_47_ALLOWED_EFFORTS: Array<ClaudeOpus47Effort> = [
70+
"low",
71+
"medium",
72+
"high",
73+
"xhigh",
74+
"max",
75+
]
76+
77+
function getAllowedClaudeEfforts(modelId: string): Array<ClaudeOpus47Effort> {
78+
if (isClaudeOpus47Model(modelId)) {
79+
return OPUS_47_ALLOWED_EFFORTS
80+
}
81+
return []
82+
}
83+
84+
function capClaudeEffort(
85+
modelId: string,
86+
effort: ClaudeOpus47Effort | undefined,
87+
): ClaudeOpus47Effort | undefined {
88+
if (!effort) {
89+
return undefined
90+
}
91+
const allowed = getAllowedClaudeEfforts(modelId)
92+
if (allowed.length === 0) {
93+
return effort
94+
}
95+
if (allowed.includes(effort)) {
96+
return effort
97+
}
98+
const capped = allowed.at(-1)
99+
consola.warn(
100+
`[${modelId}] effort "${effort}" exceeds cap; downgraded to "${capped}"`,
101+
)
102+
return capped
103+
}
104+
105+
function normalizeClaudeEffort(
106+
value: string | undefined,
107+
): ClaudeOpus47Effort | undefined {
108+
switch (value?.toLowerCase()) {
109+
case "low": {
110+
return "low"
111+
}
112+
case "medium": {
113+
return "medium"
114+
}
115+
case "high": {
116+
return "high"
117+
}
118+
case "xhigh": {
119+
return "xhigh"
120+
}
121+
case "max": {
122+
return "max"
123+
}
124+
default: {
125+
return undefined
126+
}
127+
}
128+
}
129+
130+
function getClaudeOpus47Effort(
131+
payload: AnthropicMessagesPayload,
132+
): ClaudeOpus47Effort | undefined {
133+
const explicitEffort = normalizeClaudeEffort(payload.reasoning_effort)
134+
if (explicitEffort) {
135+
return explicitEffort
136+
}
137+
138+
if (payload.thinking?.type !== "enabled") {
139+
return undefined
140+
}
141+
142+
const budgetTokens = payload.thinking.budget_tokens
143+
if (budgetTokens === undefined) {
144+
return "medium"
145+
}
146+
147+
if (budgetTokens <= 2_048) {
148+
return "low"
149+
}
150+
151+
if (budgetTokens <= 8_192) {
152+
return "medium"
153+
}
154+
155+
if (budgetTokens <= 24_576) {
156+
return "high"
157+
}
158+
159+
return "xhigh"
160+
}
161+
162+
function translateThinking(
163+
payload: AnthropicMessagesPayload,
164+
): ChatCompletionsPayload["thinking"] {
165+
const modelId = translateModelName(payload.model)
166+
167+
if (!isClaudeOpus47Model(modelId)) {
168+
return undefined
169+
}
170+
171+
const t = payload.thinking
172+
if (!t) {
173+
return undefined
174+
}
175+
176+
// Upstream Copilot opus-4.7 only accepts {type: "enabled"} (no "adaptive",
177+
// no "disabled"). The Anthropic schema admits "enabled" | "adaptive"; we
178+
// coerce both to "enabled" so legacy clients sending "adaptive" keep working.
179+
return t.budget_tokens === undefined ?
180+
{ type: "enabled" }
181+
: { type: "enabled", budget_tokens: t.budget_tokens }
182+
}
183+
184+
function translateOutputConfig(
185+
payload: AnthropicMessagesPayload,
186+
): ChatCompletionsPayload["output_config"] {
187+
const modelId = translateModelName(payload.model)
188+
189+
if (!isClaudeOpus47Model(modelId)) {
190+
return undefined
191+
}
192+
193+
const raw = getClaudeOpus47Effort(payload)
194+
const capped = capClaudeEffort(modelId, raw)
195+
196+
return capped ? { effort: capped } : undefined
197+
}
198+
199+
function translateReasoningEffort(
200+
payload: AnthropicMessagesPayload,
201+
): ChatCompletionsPayload["reasoning_effort"] {
202+
const modelId = translateModelName(payload.model)
203+
204+
if (isClaudeModel(modelId)) {
205+
return undefined
206+
}
207+
208+
if (payload.reasoning_effort) {
209+
return sanitizeReasoningEffortForModel(
210+
modelId,
211+
normalizeReasoningEffort(payload.reasoning_effort),
212+
)
213+
}
214+
215+
if (payload.thinking?.type !== "enabled") {
216+
return undefined
217+
}
218+
219+
const budgetTokens = payload.thinking.budget_tokens
220+
if (budgetTokens === undefined) {
221+
return "medium"
222+
}
223+
224+
if (budgetTokens <= 2_048) {
225+
return "low"
226+
}
227+
228+
if (budgetTokens <= 8_192) {
229+
return "medium"
230+
}
231+
232+
if (budgetTokens <= 24_576) {
233+
return sanitizeReasoningEffortForModel(modelId, "high")
234+
}
235+
236+
return sanitizeReasoningEffortForModel(modelId, "xhigh")
237+
}
238+
239+
function normalizeReasoningEffort(
240+
value: string,
241+
): ChatCompletionsPayload["reasoning_effort"] {
242+
switch (value.toLowerCase()) {
243+
case "none": {
244+
return "none"
245+
}
246+
case "low": {
247+
return "low"
248+
}
249+
case "medium": {
250+
return "medium"
251+
}
252+
case "high": {
253+
return "high"
254+
}
255+
case "xhigh": {
256+
return "xhigh"
257+
}
258+
case "max": {
259+
return "max"
260+
}
261+
default: {
262+
return undefined
263+
}
264+
}
265+
}
266+
49267
function translateModelName(model: string): string {
50-
// Claude Code subagent requests use date-suffixed model names (e.g. claude-opus-4-20250514)
51-
// which Copilot doesn't support. Map them to the latest supported versions.
52-
if (model.startsWith("claude-haiku-4-") || model === "claude-haiku-4") {
53-
return "claude-haiku-4.5"
54-
} else if (
55-
model.startsWith("claude-sonnet-4-")
56-
|| model === "claude-sonnet-4"
57-
) {
58-
return "claude-sonnet-4.6"
59-
} else if (model.startsWith("claude-opus-4-") || model === "claude-opus-4") {
60-
return "claude-opus-4.7"
61-
}
62-
return model
268+
return resolveModelId(model)
63269
}
64270

65271
function translateAnthropicMessagesToOpenAI(

0 commit comments

Comments
 (0)