Skip to content

Commit 1508fc3

Browse files
committed
support opus 4.7 and adaptive thinking
1 parent 37ccc8c commit 1508fc3

8 files changed

Lines changed: 413 additions & 5 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,14 @@ New endpoints for monitoring your Copilot usage and quotas.
210210
| `GET /usage` | `GET` | Get detailed Copilot usage statistics and quota information. |
211211
| `GET /token` | `GET` | Get the current Copilot token being used by the API. |
212212

213-
## **GPT and Gemini Support**
213+
## **GPT, Claude Opus 4.7, and Gemini Support**
214214

215215
| Model | Status | Notes |
216216
| ----- | ------ | ----- |
217217
| `gpt-5.4` | Supported | Uses the standard GPT-5 chat/completions path. |
218218
| `gpt-5.3-codex` | Supported | Uses the Responses API bridge internally. |
219219
| `gpt-5.4-mini` | Supported | Uses the Responses API bridge internally. |
220+
| `claude-opus-4.7` | Supported | Snapshot aliases such as `claude-opus-4-7-20260417` resolve to this model, with `low`, `medium`, `high`, `xhigh`, and `max` effort support. |
220221
| `gemini-3.1-pro` | Supported | Resolves to Copilot's current `gemini-3.1-pro-preview` model. |
221222
| `gemini-3-flash` | Supported | Resolves to Copilot's current `gemini-3-flash-preview` model. |
222223

@@ -231,8 +232,9 @@ Reasoning effort is model-specific.
231232
| `gpt-5.4` | `low`, `medium`, `high`, `xhigh` | `medium` |
232233
| `gpt-5.3-codex` | `low`, `medium`, `high`, `xhigh` | `medium` |
233234
| `gpt-5.4-mini` | `none`, `low`, `medium` | `medium` |
235+
| `claude-opus-4.7` | `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
234236

235-
If an unsupported effort value is sent for one of these models, the proxy falls back to that model's default `medium` instead of forwarding an invalid upstream request.
237+
If an unsupported effort value is sent for one of these models, the proxy falls back to that model's default behavior instead of forwarding an invalid upstream request.
236238

237239
### `settings.json`
238240

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"build": "tsdown",
2626
"dev": "bun run --watch ./src/main.ts",
2727
"knip": "knip-bun",
28-
"lint": "eslint --cache",
29-
"lint:all": "eslint --cache .",
28+
"lint": "eslint --cache --no-warn-ignored",
29+
"lint:all": "eslint --cache --no-warn-ignored .",
3030
"prepack": "bun run build",
3131
"prepare": "simple-git-hooks",
3232
"release": "bumpp && bun publish --access public",

src/lib/models.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const stripSnapshotSuffix = (modelId: string): string => {
1313
return "claude-sonnet-4"
1414
}
1515

16+
if (/^claude-opus-4-7-\d{8}$/.test(modelId)) {
17+
return "claude-opus-4.7"
18+
}
19+
1620
if (modelId.startsWith("claude-opus-4-")) {
1721
return "claude-opus-4"
1822
}

src/routes/messages/anthropic-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface AnthropicMessagesPayload {
1919
name?: string
2020
}
2121
thinking?: {
22-
type: "enabled"
22+
type: "enabled" | "adaptive"
2323
budget_tokens?: number
2424
}
2525
reasoning_effort?: "none" | "low" | "medium" | "high" | "max" | "xhigh"

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,123 @@ export function translateToOpenAI(
4242
stream: payload.stream,
4343
temperature: payload.temperature,
4444
top_p: payload.top_p,
45+
thinking: translateThinking(payload),
46+
output_config: translateOutputConfig(payload),
4547
reasoning_effort: translateReasoningEffort(payload),
4648
user: payload.metadata?.user_id,
4749
tools: translateAnthropicToolsToOpenAI(payload.tools),
4850
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice),
4951
}
5052
}
5153

54+
function isClaudeModel(modelId: string): boolean {
55+
return modelId.startsWith("claude-")
56+
}
57+
58+
function isClaudeOpus47Model(modelId: string): boolean {
59+
return modelId === "claude-opus-4.7"
60+
}
61+
62+
type ClaudeOpus47Effort = NonNullable<
63+
NonNullable<ChatCompletionsPayload["output_config"]>["effort"]
64+
>
65+
66+
function normalizeClaudeEffort(
67+
value: string | undefined,
68+
): ClaudeOpus47Effort | undefined {
69+
switch (value?.toLowerCase()) {
70+
case "low": {
71+
return "low"
72+
}
73+
case "medium": {
74+
return "medium"
75+
}
76+
case "high": {
77+
return "high"
78+
}
79+
case "xhigh": {
80+
return "xhigh"
81+
}
82+
case "max": {
83+
return "max"
84+
}
85+
default: {
86+
return undefined
87+
}
88+
}
89+
}
90+
91+
function getClaudeOpus47Effort(
92+
payload: AnthropicMessagesPayload,
93+
): ClaudeOpus47Effort | undefined {
94+
const explicitEffort = normalizeClaudeEffort(payload.reasoning_effort)
95+
if (explicitEffort) {
96+
return explicitEffort
97+
}
98+
99+
if (payload.thinking?.type !== "enabled") {
100+
return undefined
101+
}
102+
103+
const budgetTokens = payload.thinking.budget_tokens
104+
if (budgetTokens === undefined) {
105+
return "medium"
106+
}
107+
108+
if (budgetTokens <= 2_048) {
109+
return "low"
110+
}
111+
112+
if (budgetTokens <= 8_192) {
113+
return "medium"
114+
}
115+
116+
if (budgetTokens <= 24_576) {
117+
return "high"
118+
}
119+
120+
return "xhigh"
121+
}
122+
123+
function translateThinking(
124+
payload: AnthropicMessagesPayload,
125+
): ChatCompletionsPayload["thinking"] {
126+
const modelId = translateModelName(payload.model)
127+
128+
if (!isClaudeOpus47Model(modelId)) {
129+
return undefined
130+
}
131+
132+
if (payload.thinking?.type === "adaptive") {
133+
return { type: "adaptive" }
134+
}
135+
136+
return payload.thinking?.type === "enabled" ? { type: "adaptive" } : undefined
137+
}
138+
139+
function translateOutputConfig(
140+
payload: AnthropicMessagesPayload,
141+
): ChatCompletionsPayload["output_config"] {
142+
const modelId = translateModelName(payload.model)
143+
144+
if (!isClaudeOpus47Model(modelId)) {
145+
return undefined
146+
}
147+
148+
const effort = getClaudeOpus47Effort(payload)
149+
150+
return effort ? { effort } : undefined
151+
}
152+
52153
function translateReasoningEffort(
53154
payload: AnthropicMessagesPayload,
54155
): ChatCompletionsPayload["reasoning_effort"] {
55156
const modelId = translateModelName(payload.model)
56157

158+
if (isClaudeModel(modelId)) {
159+
return undefined
160+
}
161+
57162
if (payload.reasoning_effort) {
58163
return sanitizeReasoningEffortForModel(
59164
modelId,

src/services/copilot/create-chat-completions.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import {
1818
const usesMaxCompletionTokens = (modelId: string): boolean =>
1919
modelId.startsWith("gpt-5")
2020

21+
const isClaudeOpus47Model = (modelId: string): boolean =>
22+
modelId === "claude-opus-4.7"
23+
24+
type ClaudeOpus47Effort = NonNullable<
25+
NonNullable<ChatCompletionsPayload["output_config"]>["effort"]
26+
>
27+
2128
// Copilot rejects user identifiers longer than 64 characters.
2229
const MAX_USER_LENGTH = 64
2330

@@ -102,6 +109,47 @@ const normalizeReasoningEffort = (
102109
}
103110
}
104111

112+
const normalizeClaudeOpus47Effort = (
113+
value: string | undefined | null,
114+
): ClaudeOpus47Effort | undefined => {
115+
switch (value?.toLowerCase()) {
116+
case "low": {
117+
return "low"
118+
}
119+
case "medium": {
120+
return "medium"
121+
}
122+
case "high": {
123+
return "high"
124+
}
125+
case "xhigh": {
126+
return "xhigh"
127+
}
128+
case "max": {
129+
return "max"
130+
}
131+
default: {
132+
return undefined
133+
}
134+
}
135+
}
136+
137+
const getRequestedClaudeOpus47Effort = (
138+
payload: ChatCompletionsPayload,
139+
claudeSettingsEnv: Record<string, string>,
140+
): ClaudeOpus47Effort | undefined => {
141+
if (!isClaudeOpus47Model(payload.model)) {
142+
return undefined
143+
}
144+
145+
return (
146+
payload.output_config?.effort
147+
?? normalizeClaudeOpus47Effort(payload.reasoning_effort)
148+
?? normalizeClaudeOpus47Effort(process.env.COPILOT_REASONING_EFFORT)
149+
?? normalizeClaudeOpus47Effort(claudeSettingsEnv.COPILOT_REASONING_EFFORT)
150+
)
151+
}
152+
105153
export const sanitizeUserIdentifier = (
106154
user: string | null | undefined,
107155
): string | undefined => {
@@ -120,6 +168,10 @@ const buildRequestPayload = (
120168
payload,
121169
claudeSettingsEnv,
122170
)
171+
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(
172+
payload,
173+
claudeSettingsEnv,
174+
)
123175

124176
const reasoningEffort =
125177
(
@@ -138,6 +190,17 @@ const buildRequestPayload = (
138190
) {
139191
const sanitizedPayload = {
140192
...payload,
193+
output_config:
194+
requestedClaudeOpus47Effort ?
195+
{
196+
...payload.output_config,
197+
effort: requestedClaudeOpus47Effort,
198+
}
199+
: payload.output_config,
200+
reasoning_effort:
201+
isClaudeOpus47Model(payload.model) ? undefined : (
202+
payload.reasoning_effort
203+
),
141204
user: sanitizeUserIdentifier(payload.user),
142205
}
143206

@@ -337,6 +400,13 @@ export interface ChatCompletionsPayload {
337400
temperature?: number | null
338401
top_p?: number | null
339402
max_tokens?: number | null
403+
thinking?: {
404+
type: "enabled" | "adaptive"
405+
budget_tokens?: number
406+
} | null
407+
output_config?: {
408+
effort?: "low" | "medium" | "high" | "xhigh" | "max"
409+
} | null
340410
reasoning_effort?: "none" | "low" | "medium" | "high" | "max" | "xhigh" | null
341411
stop?: string | Array<string> | null
342412
n?: number | null

0 commit comments

Comments
 (0)