fix: route all providers through proxy to patch thinking.type for Claude 4
Opus 4.7 requires thinking.type="adaptive" instead of "enabled". The CLI sends "enabled" which upstream APIs reject. Now all providers (including Anthropic-format) route through the proxy, which patches the thinking parameter before forwarding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,17 @@ import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthr
|
|||||||
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
||||||
import type { AnthropicRequest } from './transform/types.js'
|
import type { AnthropicRequest } from './transform/types.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch thinking parameter for newer Claude models (4.x / Opus 4.7+).
|
||||||
|
* These models require `thinking.type = "adaptive"` instead of `"enabled"`.
|
||||||
|
*/
|
||||||
|
function patchThinkingForModel(body: AnthropicRequest): void {
|
||||||
|
if (!body.thinking) return
|
||||||
|
if (body.thinking.type === 'enabled') {
|
||||||
|
body.thinking.type = 'adaptive'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const providerService = new ProviderService()
|
const providerService = new ProviderService()
|
||||||
|
|
||||||
export async function handleProxyRequest(req: Request, url: URL): Promise<Response> {
|
export async function handleProxyRequest(req: Request, url: URL): Promise<Response> {
|
||||||
@@ -53,21 +64,6 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.apiFormat === 'anthropic') {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'invalid_request_error',
|
|
||||||
message: providerId
|
|
||||||
? `Provider "${providerId}" uses anthropic format — proxy not needed`
|
|
||||||
: 'Active provider uses anthropic format — proxy not needed',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
let body: AnthropicRequest
|
let body: AnthropicRequest
|
||||||
try {
|
try {
|
||||||
@@ -82,8 +78,13 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|||||||
const isStream = body.stream === true
|
const isStream = body.stream === true
|
||||||
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
||||||
|
|
||||||
|
// Always patch thinking parameter for newer models
|
||||||
|
patchThinkingForModel(body)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (config.apiFormat === 'openai_chat') {
|
if (config.apiFormat === 'anthropic') {
|
||||||
|
return await handleAnthropicPassthrough(req, body, baseUrl, config.apiKey, isStream)
|
||||||
|
} else if (config.apiFormat === 'openai_chat') {
|
||||||
return await handleOpenaiChat(body, baseUrl, config.apiKey, isStream)
|
return await handleOpenaiChat(body, baseUrl, config.apiKey, isStream)
|
||||||
} else {
|
} else {
|
||||||
return await handleOpenaiResponses(body, baseUrl, config.apiKey, isStream)
|
return await handleOpenaiResponses(body, baseUrl, config.apiKey, isStream)
|
||||||
@@ -103,6 +104,50 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAnthropicPassthrough(
|
||||||
|
originalReq: Request,
|
||||||
|
body: AnthropicRequest,
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
isStream: boolean,
|
||||||
|
): Promise<Response> {
|
||||||
|
const url = `${baseUrl}/v1/messages`
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiKey,
|
||||||
|
'anthropic-version': originalReq.headers.get('anthropic-version') || '2023-06-01',
|
||||||
|
}
|
||||||
|
const authHeader = originalReq.headers.get('authorization')
|
||||||
|
if (authHeader) {
|
||||||
|
headers['Authorization'] = authHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(600_000),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isStream && upstream.ok && upstream.body) {
|
||||||
|
return new Response(upstream.body, {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': upstream.headers.get('Content-Type') || 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await upstream.text()
|
||||||
|
return new Response(text, {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOpenaiChat(
|
async function handleOpenaiChat(
|
||||||
body: AnthropicRequest,
|
body: AnthropicRequest,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export class ProviderService {
|
|||||||
provider: SavedProvider,
|
provider: SavedProvider,
|
||||||
options?: { proxyPath?: string },
|
options?: { proxyPath?: string },
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const needsProxy = provider.apiFormat != null && provider.apiFormat !== 'anthropic'
|
const needsProxy = provider.apiFormat != null
|
||||||
const proxyPath = options?.proxyPath ?? '/proxy'
|
const proxyPath = options?.proxyPath ?? '/proxy'
|
||||||
const baseUrl = needsProxy
|
const baseUrl = needsProxy
|
||||||
? `http://127.0.0.1:${ProviderService.serverPort}${proxyPath}`
|
? `http://127.0.0.1:${ProviderService.serverPort}${proxyPath}`
|
||||||
|
|||||||
Reference in New Issue
Block a user