diff --git a/routes/admin.py b/routes/admin.py index 81702aa..e9241b0 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -137,6 +137,8 @@ def add_mapping(): 'api_key': data.get('api_key', ''), 'custom_instructions': data.get('custom_instructions', ''), 'instructions_position': data.get('instructions_position', 'prepend'), + 'body_modifications': data.get('body_modifications') or {}, + 'header_modifications': data.get('header_modifications') or {}, } return _save_and_respond(s, f'映射已添加: {name}') @@ -161,6 +163,8 @@ def update_mapping(name): 'api_key': data.get('api_key', ''), 'custom_instructions': data.get('custom_instructions', ''), 'instructions_position': data.get('instructions_position', 'prepend'), + 'body_modifications': data.get('body_modifications') or {}, + 'header_modifications': data.get('header_modifications') or {}, } if new_name != name: del mappings[name] diff --git a/routes/chat.py b/routes/chat.py index 3f695a4..6ef1f7a 100644 --- a/routes/chat.py +++ b/routes/chat.py @@ -28,6 +28,8 @@ from adapters.responses_cc_adapter import ( from config import Config from routes.common import ( RouteContext, + apply_body_modifications, + apply_header_modifications, build_anthropic_target, build_openai_target, build_responses_target, @@ -118,6 +120,8 @@ def _handle_openai_backend(ctx: RouteContext, payload: dict[str, Any]): ) url, headers = build_openai_target(ctx) + payload = apply_body_modifications(payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_openai_stream(ctx, payload, url, headers) @@ -208,6 +212,8 @@ def _handle_responses_backend(ctx: RouteContext, payload: dict[str, Any]): ) url, headers = build_responses_target(ctx) + responses_payload = apply_body_modifications(responses_payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_responses_stream(ctx, responses_payload, url, headers) @@ -285,6 +291,8 @@ def _handle_anthropic_backend(ctx: RouteContext, payload: dict[str, Any]): ) url, headers = build_anthropic_target(ctx) + anthropic_payload = apply_body_modifications(anthropic_payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_anthropic_stream(ctx, anthropic_payload, url, headers) diff --git a/routes/common.py b/routes/common.py index 4855db1..3ddcb72 100644 --- a/routes/common.py +++ b/routes/common.py @@ -34,6 +34,8 @@ class RouteContext: is_stream: bool custom_instructions: str instructions_position: str + body_modifications: dict + header_modifications: dict def build_route_context(client_model: str, is_stream: bool) -> RouteContext: @@ -48,6 +50,8 @@ def build_route_context(client_model: str, is_stream: bool) -> RouteContext: is_stream=is_stream, custom_instructions=mapping.get('custom_instructions', ''), instructions_position=mapping.get('instructions_position', 'prepend'), + body_modifications=mapping.get('body_modifications', {}), + header_modifications=mapping.get('header_modifications', {}), ) @@ -193,3 +197,38 @@ def inject_instructions_anthropic(payload: dict[str, Any], instructions: str, po logger.info('已注入自定义指令到 Anthropic system (%d 字符, %s)', len(instructions), position) return payload + + +# ─── Body / Header 修改 ────────────────────────── + + +def apply_body_modifications(payload: dict[str, Any], modifications: dict[str, Any]) -> dict[str, Any]: + """对转发请求体应用字段级修改。 + + 规则与 CursorProxy 一致:值为 null 的字段会被删除,其余字段设置/覆盖。 + """ + if not modifications: + return payload + for key, value in modifications.items(): + if value is None: + payload.pop(key, None) + else: + payload[key] = value + logger.info('已应用 body_modifications: %s', list(modifications.keys())) + return payload + + +def apply_header_modifications(headers: dict[str, str], modifications: dict[str, Any]) -> dict[str, str]: + """对转发请求头应用字段级修改。 + + 规则同 body:值为 null 删除,其余设置/覆盖。 + """ + if not modifications: + return headers + for key, value in modifications.items(): + if value is None: + headers.pop(key, None) + else: + headers[key] = str(value) + logger.info('已应用 header_modifications: %s', list(modifications.keys())) + return headers diff --git a/routes/messages.py b/routes/messages.py index 0a010ec..73637a2 100644 --- a/routes/messages.py +++ b/routes/messages.py @@ -13,7 +13,7 @@ from flask import Blueprint, request, jsonify import settings from config import Config -from routes.common import inject_instructions_anthropic +from routes.common import apply_body_modifications, apply_header_modifications, inject_instructions_anthropic from utils.http import build_anthropic_headers, forward_request, sse_response logger = logging.getLogger(__name__) @@ -35,10 +35,14 @@ def messages_passthrough(): api_key = mapping['api_key'] custom_instructions = mapping.get('custom_instructions', '') instructions_position = mapping.get('instructions_position', 'prepend') + body_mods = mapping.get('body_modifications', {}) + header_mods = mapping.get('header_modifications', {}) headers = build_anthropic_headers(api_key) + headers = apply_header_modifications(headers, header_mods) url = f'{url_base.rstrip("/")}/v1/messages' payload = inject_instructions_anthropic(payload, custom_instructions, instructions_position) + payload = apply_body_modifications(payload, body_mods) if not is_stream: resp, err = forward_request(url, headers, payload) diff --git a/routes/responses.py b/routes/responses.py index 3548e96..e1b4b62 100644 --- a/routes/responses.py +++ b/routes/responses.py @@ -18,6 +18,8 @@ from adapters.responses_cc_adapter import ResponsesStreamConverter, cc_to_respon from config import Config from routes.common import ( RouteContext, + apply_body_modifications, + apply_header_modifications, build_anthropic_target, build_openai_target, build_responses_target, @@ -93,6 +95,8 @@ def _handle_openai_backend(ctx: RouteContext, cc_payload: dict[str, Any]): ) url, headers = build_openai_target(ctx) + cc_payload = apply_body_modifications(cc_payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_openai_stream(ctx, cc_payload, url, headers) @@ -177,6 +181,8 @@ def _handle_responses_backend(ctx: RouteContext, payload: dict[str, Any]): payload['model'] = ctx.upstream_model payload = inject_instructions_responses(payload, ctx.custom_instructions, ctx.instructions_position) url, headers = build_responses_target(ctx) + payload = apply_body_modifications(payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_responses_stream(ctx, payload, url, headers) @@ -241,6 +247,8 @@ def _handle_anthropic_backend(ctx: RouteContext, cc_payload: dict[str, Any]): ) url, headers = build_anthropic_target(ctx) + anthropic_payload = apply_body_modifications(anthropic_payload, ctx.body_modifications) + headers = apply_header_modifications(headers, ctx.header_modifications) if ctx.is_stream: return _handle_anthropic_stream(ctx, anthropic_payload, url, headers) diff --git a/settings.py b/settings.py index 58e657e..0ae6939 100644 --- a/settings.py +++ b/settings.py @@ -95,6 +95,8 @@ def resolve_model(model_name): 'api_key': m.get('api_key') or base_key, 'custom_instructions': m.get('custom_instructions') or '', 'instructions_position': m.get('instructions_position') or 'prepend', + 'body_modifications': m.get('body_modifications') or {}, + 'header_modifications': m.get('header_modifications') or {}, } return { @@ -104,6 +106,8 @@ def resolve_model(model_name): 'api_key': base_key, 'custom_instructions': '', 'instructions_position': 'prepend', + 'body_modifications': {}, + 'header_modifications': {}, } diff --git a/static/admin.css b/static/admin.css index b31fd9c..b68fc96 100644 --- a/static/admin.css +++ b/static/admin.css @@ -62,6 +62,7 @@ main{padding:28px 0 60px} .tag-auto{background:rgba(139,92,246,.15);color:#a78bfa} .tag-override{background:rgba(59,130,246,.1);color:var(--primary)} .tag-instructions{background:rgba(234,179,8,.15);color:var(--yellow)} +.tag-mods{background:rgba(168,85,247,.15);color:#c084fc} .mapping-actions{margin-left:auto;display:flex;gap:6px} .empty{text-align:center;padding:40px;color:var(--muted)} diff --git a/static/admin.html b/static/admin.html index 725ea25..3c2f62e 100644 --- a/static/admin.html +++ b/static/admin.html @@ -133,6 +133,22 @@
前置:自定义指令放在系统提示词最前面,模型优先看到;后置:放在末尾
+
+ +
+
+ 对转发到上游的请求体做字段级增删改。值为 null 表示删除该字段,其余为设置/覆盖。
+ 适用场景:注入 reasoning_effort、删除上游不支持的 stream_options 等。 +
+
+
+ +
+
+ 对转发到上游的请求头做增删改,规则同 Body 修改。
+ 适用场景:按模型设置不同的 Authorization、API 版本头等。 +
+