feat(claude): merge client anthropic-beta flags and forward rate-limit headers
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, PROVIDER_OAUTH } from "../config/providers.js";
|
||||
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE, selectAnthropicBeta } from "../providers/shared.js";
|
||||
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE, selectAnthropicBeta, mergeAnthropicBeta } from "../providers/shared.js";
|
||||
import { resolveOpenAICompatibleApiType } from "../services/provider.js";
|
||||
import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js";
|
||||
import { buildClineHeaders } from "../shared/clineAuth.js";
|
||||
@@ -164,9 +164,12 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
// a node fronting Kimi or GLM answers on its own ids and never matches, so
|
||||
// gateways that would choke on unknown beta flags are left untouched.
|
||||
const isClaudeModel = typeof model === "string" && /^claude-/.test(model);
|
||||
const clientBeta = credentials?.rawHeaders?.["anthropic-beta"];
|
||||
if (model && (this.provider === "claude"
|
||||
|| (this.provider?.startsWith?.("anthropic-compatible-") && isClaudeModel))) {
|
||||
headers["Anthropic-Beta"] = selectAnthropicBeta(model, body);
|
||||
headers["Anthropic-Beta"] = mergeAnthropicBeta(selectAnthropicBeta(model, body), clientBeta);
|
||||
} else if (this.provider === "anthropic" && clientBeta) {
|
||||
headers["Anthropic-Beta"] = mergeAnthropicBeta(headers["Anthropic-Beta"], clientBeta);
|
||||
}
|
||||
|
||||
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createRequestLogger } from "../utils/requestLogger.js";
|
||||
import { getModelTargetFormat, getModelSupportedFormats, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
|
||||
import { PROVIDERS } from "../config/providers.js";
|
||||
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
|
||||
import { upstreamResponseHeaders } from "../utils/upstreamHeaders.js";
|
||||
import { HTTP_STATUS, TOKEN_SAVER_HEADER } from "../config/runtimeConfig.js";
|
||||
import { handleBypassRequest } from "../utils/bypassHandler.js";
|
||||
import { trackPendingRequest, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
|
||||
@@ -486,7 +487,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
||||
log.errorLine(reqTag, "✗", `ERROR ${statusCode} · ${provider}/${model} · ${Date.now() - requestStartTime}ms${urlStr}\n ${errMsg}`);
|
||||
}
|
||||
reqLogger.logError(new Error(message), finalBody || translatedBody);
|
||||
return createErrorResult(statusCode, errMsg, resetsAtMs);
|
||||
return createErrorResult(statusCode, errMsg, resetsAtMs, upstreamResponseHeaders(providerResponse.headers));
|
||||
}
|
||||
|
||||
const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, pxpipe: pxpipeSummary, reqTag, log };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fromOpenAIFinish } from "../../translator/concerns/finishReason.js";
|
||||
import { ollamaBodyToOpenAI } from "../../translator/response/ollama-to-openai.js";
|
||||
import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js";
|
||||
import { createErrorResult } from "../../utils/error.js";
|
||||
import { upstreamResponseHeaders } from "../../utils/upstreamHeaders.js";
|
||||
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
|
||||
import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js";
|
||||
import { unwrapClineEnvelope } from "../../shared/clineEnvelope.js";
|
||||
@@ -399,7 +400,7 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
|
||||
return {
|
||||
success: true,
|
||||
response: new Response(JSON.stringify(restoreToolNames(translatedResponse, toolNameMap)), {
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", ...upstreamResponseHeaders(providerResponse.headers) }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { buildStreamErrorBytes } from "../../utils/streamHelpers.js";
|
||||
import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js";
|
||||
import { saveRequestDetail } from "@/lib/usageDb.js";
|
||||
import { SSE_HEADERS_CORS as SSE_HEADERS } from "../../utils/sseConstants.js";
|
||||
import { upstreamResponseHeaders } from "../../utils/upstreamHeaders.js";
|
||||
|
||||
// Codex returns Responses API SSE → which client format to translate INTO, by request sourceFormat.
|
||||
// Gemini-family all map to ANTIGRAVITY decoder; unknown sources fall back to OPENAI.
|
||||
@@ -109,7 +110,7 @@ export async function handleStreamingResponse({ providerResponse, provider, mode
|
||||
|
||||
return {
|
||||
success: true,
|
||||
response: new Response(transformedBody, { headers: SSE_HEADERS })
|
||||
response: new Response(transformedBody, { headers: { ...SSE_HEADERS, ...upstreamResponseHeaders(providerResponse.headers) } })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,11 @@ export function selectAnthropicBeta(model = "", body = null) {
|
||||
return flags.join(",");
|
||||
}
|
||||
|
||||
export function mergeAnthropicBeta(...values) {
|
||||
const flags = values.flatMap((v) => (typeof v === "string" ? v.split(",") : [])).map((f) => f.trim()).filter(Boolean);
|
||||
return [...new Set(flags)].join(",");
|
||||
}
|
||||
|
||||
// Shared baseUrls
|
||||
export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages";
|
||||
|
||||
|
||||
@@ -27,12 +27,13 @@ export function buildErrorBody(statusCode, message) {
|
||||
* @param {string} message - Error message
|
||||
* @returns {Response} HTTP Response object
|
||||
*/
|
||||
export function errorResponse(statusCode, message) {
|
||||
export function errorResponse(statusCode, message, extraHeaders = null) {
|
||||
return new Response(JSON.stringify(buildErrorBody(statusCode, message)), {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
...extraHeaders
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -95,13 +96,13 @@ export async function parseUpstreamError(response, executor = null) {
|
||||
* @param {number} [resetsAtMs] - Optional precise cooldown expiry (ms epoch) for provider-specific quota errors
|
||||
* @returns {{ success: false, status: number, error: string, response: Response, resetsAtMs?: number }}
|
||||
*/
|
||||
export function createErrorResult(statusCode, message, resetsAtMs) {
|
||||
export function createErrorResult(statusCode, message, resetsAtMs, extraHeaders = null) {
|
||||
return {
|
||||
success: false,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
resetsAtMs,
|
||||
response: errorResponse(statusCode, message)
|
||||
response: errorResponse(statusCode, message, extraHeaders)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ export function createErrorResult(statusCode, message, resetsAtMs) {
|
||||
* @param {string} retryAfterHuman - Human-readable retry info e.g. "reset after 30s"
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman) {
|
||||
export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman, extraHeaders = null) {
|
||||
const retryAfterSec = Math.max(Math.ceil((new Date(retryAfter).getTime() - Date.now()) / 1000), 1);
|
||||
const msg = `${message} (${retryAfterHuman})`;
|
||||
return new Response(
|
||||
@@ -121,8 +122,10 @@ export function unavailableResponse(statusCode, message, retryAfter, retryAfterH
|
||||
{
|
||||
status: statusCode,
|
||||
headers: {
|
||||
...extraHeaders,
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": String(retryAfterSec)
|
||||
// Intentionally mis-cased to prevent duplicate headers
|
||||
"retry-after": String(retryAfterSec)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
12
open-sse/utils/upstreamHeaders.js
Normal file
12
open-sse/utils/upstreamHeaders.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const FORWARDED = new Set(["retry-after", "x-should-retry"]);
|
||||
const FORWARDED_PREFIX = "anthropic-ratelimit-";
|
||||
|
||||
export function upstreamResponseHeaders(headers) {
|
||||
const out = {};
|
||||
if (typeof headers?.forEach !== "function") return out;
|
||||
headers.forEach((value, name) => {
|
||||
const key = name.toLowerCase();
|
||||
if (FORWARDED.has(key) || key.startsWith(FORWARDED_PREFIX)) out[key] = value;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user