Skip to content

Commit a356a24

Browse files
committed
improvement and opus 4.7 effort change
1 parent 0b5e132 commit a356a24

20 files changed

Lines changed: 490 additions & 297 deletions

README.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Common:
219219
| `--host <host>` | Bind address. Defaults to `127.0.0.1`. |
220220
| `--port <port>` | Listen port. Overrides `$PORT` and the port inferred from Claude settings. |
221221
| `--model <model>` | Override the request model for this bridge process only; does not edit config files. |
222+
| `--auto` | Acquire a Copilot Auto session and attach its session token only to upstream `/chat/completions` and `/responses` requests. Codex model selection is limited to Auto-available models. |
222223
| `--rate-limit <seconds>` | Enforce a minimum delay between upstream requests. |
223224
| `--wait` | With `--rate-limit`, wait instead of returning HTTP 429. |
224225

@@ -280,22 +281,15 @@ accepts upstream.
280281
| Model | Reasoning efforts | Notes |
281282
| -------------------------------- | --------------------------------------- | -------------------------------------- |
282283
| `claude-opus-4.8` | `medium` | |
283-
| `claude-opus-4.7` | `medium` | Effort sent as `output_config.effort`. |
284-
| `claude-opus-4.7-1m` | `low`, `medium`, `high`, `xhigh` | 1M-token context window, prefer use `claude-opus-4.7-[1m]` in config|
285-
| `claude-opus-4.7-high` | `high` | Fixed high reasoning|
286-
| `claude-opus-4.7-xhigh` | `xhigh` | Fixed extra-high reasoning|
284+
| `claude-opus-4.7` | `low`, `medium`, `high`, `xhigh` | Effort sent as `output_config.effort`. |
285+
| `claude-opus-4.7-1m` | `low`, `medium`, `high`, `xhigh` | 1M-token context window, prefer use `claude-opus-4.7-[1m]` in config. |
287286
| `claude-opus-4.6` | `low`, `medium`, `high` | |
288287
| `claude-opus-4.6-1m` | `low`, `medium`, `high` | 1M-token context window, prefer use `claude-opus-4.6-[1m]` in config |
289288
| `claude-sonnet-4.6` | `low`, `medium`, `high` | |
290289
| `claude-opus-4.5` || Reasoning not accepted upstream. |
291290
| `claude-sonnet-4.5` || Reasoning not accepted upstream. |
292-
| `claude-sonnet-4` || Reasoning not accepted upstream. |
293291
| `claude-haiku-4.5` || Reasoning not accepted upstream. |
294292

295-
For Claude Opus 4.7, both Codex CLI and Claude Code can use
296-
`claude-opus-4.7` with reasoning effort `high` or `xhigh`; the bridge routes the
297-
request to the matching upstream reasoning variant.
298-
299293
For Claude Code settings, prefer `claude-opus-4.7-[1m]` or
300294
`claude-opus-4.6-[1m]` when you want the CLI `/context` UI and the upstream
301295
model to both use 1M context. Direct API clients can use

scripts/probe-claude-cli.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ const CASES: Case[] = [
1919
{ model: "gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
2020
{ model: "gpt-5-mini", efforts: ["low", "medium", "high"] },
2121
// Claude
22-
{ model: "claude-opus-4.7", efforts: ["medium"] },
22+
{ model: "claude-opus-4.7", efforts: ["low", "medium", "high", "xhigh"] },
2323
{ model: "claude-opus-4.7-1m", efforts: ["low", "medium", "high", "xhigh"] },
24-
{ model: "claude-opus-4.7-high", efforts: ["high"] },
25-
{ model: "claude-opus-4.7-xhigh", efforts: ["xhigh"] },
2624
{ model: "claude-opus-4.6", efforts: ["low", "medium", "high"] },
2725
{ model: "claude-opus-4.6-1m", efforts: ["low", "medium", "high"] },
2826
{ model: "claude-sonnet-4.6", efforts: ["low", "medium", "high"] },
2927
{ model: "claude-opus-4.5", efforts: [null] },
3028
{ model: "claude-sonnet-4.5", efforts: [null] },
31-
{ model: "claude-sonnet-4", efforts: [null] },
3229
{ model: "claude-haiku-4.5", efforts: [null] },
3330
// Gemini
3431
{ model: "gemini-3.1-pro-preview", efforts: [null] },

scripts/probe-claude.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ interface Case {
88
}
99

1010
const CASES: Case[] = [
11-
{ model: "claude-opus-4.7", efforts: ["medium"] },
11+
{ model: "claude-opus-4.7", efforts: ["low", "medium", "high", "xhigh"] },
1212
{ model: "claude-opus-4.7-1m", efforts: ["low", "medium", "high", "xhigh"] },
13-
{ model: "claude-opus-4.7-high", efforts: ["high"] },
14-
{ model: "claude-opus-4.7-xhigh", efforts: ["xhigh"] },
1513
{ model: "claude-opus-4.6", efforts: ["low", "medium", "high"] },
1614
{ model: "claude-opus-4.6-1m", efforts: ["low", "medium", "high"] },
1715
{ model: "claude-sonnet-4.6", efforts: ["low", "medium", "high"] },
1816
{ model: "claude-opus-4.5", efforts: [null] },
1917
{ model: "claude-sonnet-4.5", efforts: [null] },
20-
{ model: "claude-sonnet-4", efforts: [null] },
2118
{ model: "claude-haiku-4.5", efforts: [null] },
2219
]
2320

src/bridges/claude/non-stream-translation.ts

Lines changed: 5 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -81,64 +81,6 @@ const normalizeClaudeModelAlias = (model: string): string => {
8181
return prefixed
8282
}
8383

84-
function normalizeClaudeReasoningEffortForRouting(
85-
value: string | undefined,
86-
): ClaudeOpus47Effort | undefined {
87-
switch (value?.toLowerCase()) {
88-
case "low":
89-
case "medium":
90-
case "high":
91-
case "xhigh":
92-
case "max": {
93-
return value.toLowerCase() as ClaudeOpus47Effort
94-
}
95-
default: {
96-
return undefined
97-
}
98-
}
99-
}
100-
101-
const getEnvValueCaseInsensitive = (
102-
env: Record<string, string>,
103-
key: string,
104-
): string | undefined => {
105-
const direct = env[key]
106-
if (typeof direct === "string") {
107-
return direct
108-
}
109-
const lower = key.toLowerCase()
110-
const matched = Object.entries(env).find(([k]) => k.toLowerCase() === lower)
111-
return matched?.[1]
112-
}
113-
114-
const getConfiguredClaudeReasoningEffort = (
115-
settings: Pick<ClaudeSettings, "env"> | undefined,
116-
): string | undefined =>
117-
process.env.MODEL_REASONING_EFFORT
118-
?? getEnvValueCaseInsensitive(settings?.env ?? {}, "MODEL_REASONING_EFFORT")
119-
120-
const routeClaudeOpus47ByEffort = (
121-
model: string,
122-
requestedEffort: string | undefined,
123-
): string => {
124-
if (model !== "claude-opus-4.7") {
125-
return model
126-
}
127-
128-
switch (normalizeClaudeReasoningEffortForRouting(requestedEffort)) {
129-
case "high": {
130-
return "claude-opus-4.7-high"
131-
}
132-
case "xhigh":
133-
case "max": {
134-
return "claude-opus-4.7-xhigh"
135-
}
136-
default: {
137-
return model
138-
}
139-
}
140-
}
141-
14284
const getConfiguredClaudeDefaultModel = (
14385
settings: Pick<ClaudeSettings, "env" | "model"> | undefined,
14486
): string | undefined => {
@@ -191,15 +133,10 @@ const resolveClaudeRequestedModel = (
191133
export function translateModelName(
192134
model: string,
193135
settings?: Pick<ClaudeSettings, "env" | "model">,
194-
requestedReasoningEffort?: string,
195136
): string {
196137
const requestedModel = resolveClaudeRequestedModel(model, settings)
197138
const normalizedModel = normalizeClaudeModelAlias(requestedModel)
198-
const routedModel = routeClaudeOpus47ByEffort(
199-
normalizedModel,
200-
requestedReasoningEffort ?? getConfiguredClaudeReasoningEffort(settings),
201-
)
202-
return resolveUpstreamModelId(routedModel)
139+
return resolveUpstreamModelId(normalizedModel)
203140
}
204141

205142
function isClaudeModel(modelId: string): boolean {
@@ -275,7 +212,7 @@ function translateThinking(
275212
payload: AnthropicMessagesPayload,
276213
settings?: Pick<ClaudeSettings, "env" | "model">,
277214
): ChatCompletionsPayload["thinking"] {
278-
const modelId = translateModelName(payload.model, settings, payload.reasoning_effort)
215+
const modelId = translateModelName(payload.model, settings)
279216

280217
if (!isClaudeOpus47Model(modelId)) {
281218
return undefined
@@ -292,7 +229,7 @@ function translateOutputConfig(
292229
payload: AnthropicMessagesPayload,
293230
settings?: Pick<ClaudeSettings, "env" | "model">,
294231
): ChatCompletionsPayload["output_config"] {
295-
const modelId = translateModelName(payload.model, settings, payload.reasoning_effort)
232+
const modelId = translateModelName(payload.model, settings)
296233

297234
if (!isClaudeOpus47Model(modelId)) {
298235
return undefined
@@ -335,7 +272,7 @@ function translateReasoningEffort(
335272
payload: AnthropicMessagesPayload,
336273
settings?: Pick<ClaudeSettings, "env" | "model">,
337274
): ChatCompletionsPayload["reasoning_effort"] {
338-
const modelId = translateModelName(payload.model, settings, payload.reasoning_effort)
275+
const modelId = translateModelName(payload.model, settings)
339276

340277
if (isClaudeOpus47Model(modelId)) {
341278
return undefined
@@ -379,7 +316,7 @@ export function translateToOpenAI(
379316
settings?: Pick<ClaudeSettings, "env" | "model">,
380317
toolNameMapper?: AnthropicToolNameMapper,
381318
): ChatCompletionsPayload {
382-
const model = translateModelName(payload.model, settings, payload.reasoning_effort)
319+
const model = translateModelName(payload.model, settings)
383320
const mapper = toolNameMapper ?? createAnthropicToolNameMapper(payload.tools, {
384321
...getToolNameMapperOptionsForModel(model),
385322
})

src/bridges/claude/tool-names.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ export const getToolNameMapperOptionsForModel = (
3131
return { allowDots: false, maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH }
3232
}
3333

34-
if (/^claude-sonnet-4(?:$|-\d{8}$)/.test(normalized)) {
35-
return { allowDots: false, maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH }
36-
}
37-
3834
if (normalized.startsWith("gemini-")) {
3935
return { allowDots: true, maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH }
4036
}

src/bridges/codex/responses.ts

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,43 +36,14 @@ const removeReasoningEffort = (reasoning: ReasoningField): ReasoningField | unde
3636
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
3737
typeof value === "object" && value !== null && !Array.isArray(value)
3838

39-
const routeClaudeOpus47ByReasoningEffort = (
40-
model: string,
41-
effort: unknown,
42-
): string => {
43-
if (model !== "claude-opus-4.7") {
44-
return model
45-
}
46-
47-
switch (typeof effort === "string" ? effort.toLowerCase() : undefined) {
48-
case "high": {
49-
return "claude-opus-4.7-high"
50-
}
51-
case "xhigh":
52-
case "max": {
53-
return "claude-opus-4.7-xhigh"
54-
}
55-
default: {
56-
return model
57-
}
58-
}
59-
}
60-
6139
export const normalizeCodexResponsesRequest = (
6240
payload: CodexResponsesRequest,
6341
configuredReasoningEffort?: unknown,
6442
): CodexResponsesRequest => {
6543
const parsed = codexResponsesRequestSchema.parse(payload) as CodexResponsesRequest
6644
& { reasoning?: ReasoningField; text?: TextField }
6745

68-
const incomingReasoning =
69-
isPlainObject(parsed.reasoning) ? (parsed.reasoning as ReasoningField) : undefined
70-
const requestedReasoningEffort =
71-
incomingReasoning?.effort ?? configuredReasoningEffort
72-
const canonical = routeClaudeOpus47ByReasoningEffort(
73-
resolveModelId(parsed.model),
74-
requestedReasoningEffort,
75-
)
46+
const canonical = resolveModelId(parsed.model)
7647
const capability = getModelCapability(canonical)
7748
if (!capability) return parsed
7849

@@ -87,10 +58,11 @@ export const normalizeCodexResponsesRequest = (
8758

8859
if ("reasoning" in next) {
8960
if (!isPlainObject(next.reasoning)) {
90-
if (configuredReasoningEffort === undefined || configuredReasoningEffort === null) {
61+
const effort = configuredReasoningEffort
62+
if (effort === undefined || effort === null) {
9163
delete (next as Record<string, unknown>).reasoning
9264
} else {
93-
const clamped = clampReasoningEffort(canonical, configuredReasoningEffort)
65+
const clamped = clampReasoningEffort(canonical, effort)
9466
if (clamped) {
9567
next.reasoning = { effort: clamped.effort }
9668
}
@@ -112,8 +84,13 @@ export const normalizeCodexResponsesRequest = (
11284
}
11385
}
11486
}
115-
} else if (configuredReasoningEffort !== undefined && configuredReasoningEffort !== null) {
116-
const clamped = clampReasoningEffort(canonical, configuredReasoningEffort)
87+
} else if (
88+
configuredReasoningEffort !== undefined && configuredReasoningEffort !== null
89+
) {
90+
const clamped = clampReasoningEffort(
91+
canonical,
92+
configuredReasoningEffort,
93+
)
11794
if (clamped) {
11895
next.reasoning = { effort: clamped.effort }
11996
}

src/lib/auto-session.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface AutoSessionResponse {
1111

1212
const AUTO_MODE_BODY = { auto_mode: { model_hints: ["auto"] } }
1313
const FALLBACK_REFRESH_SECONDS = 30 * 60
14+
let refreshTimer: ReturnType<typeof setTimeout> | undefined
1415

1516
const parseExpiresAt = (value: number | string | undefined): number | undefined => {
1617
if (typeof value === "number" && Number.isFinite(value)) {
@@ -68,12 +69,16 @@ const applyAutoSession = async (config: BridgeConfig) => {
6869
}
6970

7071
const scheduleAutoSessionRefresh = (config: BridgeConfig) => {
72+
if (refreshTimer) {
73+
clearTimeout(refreshTimer)
74+
}
75+
7176
const now = Math.floor(Date.now() / 1000)
7277
const expiresAt =
7378
runtimeState.autoExpiresAt ?? now + FALLBACK_REFRESH_SECONDS
7479
const refreshIn = Math.max(expiresAt - now - 60, 60)
7580

76-
const timer = setTimeout(async () => {
81+
refreshTimer = setTimeout(async () => {
7782
try {
7883
await applyAutoSession(config)
7984
consola.debug("Refreshed Copilot auto-mode session token")
@@ -84,14 +89,26 @@ const scheduleAutoSessionRefresh = (config: BridgeConfig) => {
8489
}
8590
}, refreshIn * 1000)
8691

87-
if (typeof timer.unref === "function") {
88-
timer.unref()
92+
if (typeof refreshTimer.unref === "function") {
93+
refreshTimer.unref()
94+
}
95+
}
96+
97+
export const disableAutoMode = () => {
98+
if (refreshTimer) {
99+
clearTimeout(refreshTimer)
100+
refreshTimer = undefined
89101
}
102+
delete runtimeState.autoMode
103+
delete runtimeState.autoSessionToken
104+
delete runtimeState.autoExpiresAt
105+
delete runtimeState.autoAvailableModels
90106
}
91107

92108
export const enableAutoMode = async (
93109
config: BridgeConfig,
94110
): Promise<AutoSessionResponse> => {
111+
disableAutoMode()
95112
const data = await applyAutoSession(config)
96113
runtimeState.autoMode = true
97114
scheduleAutoSessionRefresh(config)

src/lib/model-capabilities.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,17 @@ export const MODEL_CAPABILITIES: ReadonlyArray<ModelCapability> = [
102102
// translate to /v1/chat/completions. Only opus-4.7 places effort under
103103
// output_config.effort; the others use the standard reasoning_effort.
104104
...[
105-
{ id: "claude-opus-4.7", supported: ["medium"], defaultEffort: "medium" },
105+
{
106+
id: "claude-opus-4.7",
107+
supported: ["low", "medium", "high", "xhigh"],
108+
defaultEffort: "medium",
109+
},
106110
{
107111
id: "claude-opus-4.7-1m-internal",
108112
aliases: ["claude-opus-4.7-1m"],
109113
supported: ["low", "medium", "high", "xhigh"],
110114
defaultEffort: "medium",
111115
},
112-
{ id: "claude-opus-4.7-high", supported: ["high"], defaultEffort: "high" },
113-
{ id: "claude-opus-4.7-xhigh", supported: ["xhigh"], defaultEffort: "xhigh" },
114116
].map(
115117
({ id, aliases, supported, defaultEffort }): ModelCapability => ({
116118
id,
@@ -163,10 +165,6 @@ export const MODEL_CAPABILITIES: ReadonlyArray<ModelCapability> = [
163165
id: "claude-sonnet-4.5",
164166
fallback: "chat-completions",
165167
},
166-
{
167-
id: "claude-sonnet-4",
168-
fallback: "chat-completions",
169-
},
170168
{
171169
id: "claude-haiku-4.5",
172170
fallback: "chat-completions",

src/lib/models-resolver.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ const pickBestModel = (models: Array<Model>): Model | undefined =>
7474
(left, right) => scoreModelCandidate(right) - scoreModelCandidate(left),
7575
)[0]
7676

77+
const isClaudeMajorOnlyVersion = (modelId: string): boolean =>
78+
/^claude-(?:opus|sonnet|haiku)-\d+$/.test(modelId)
79+
7780
const getBestPrefixMatches = (
7881
models: Array<Model>,
7982
aliasCandidates: Array<string>,
@@ -86,6 +89,9 @@ const getBestPrefixMatches = (
8689
const matchedAliasLength = Math.max(
8790
0,
8891
...aliasCandidates.map((candidate) => {
92+
if (isClaudeMajorOnlyVersion(candidate)) {
93+
return 0
94+
}
8995
const normalizedCandidate = normalizeModelId(candidate)
9096
return normalizedCandidate.length >= 4
9197
&& normalizedModelId.startsWith(normalizedCandidate)

0 commit comments

Comments
 (0)