跨协议转换与人工提交的竞态
三种协议的字段矩阵、200 字符伪流式、first_reply_wins 的条件更新,以及工具调用的信任边界。
一条请求从 OpenAI/Anthropic SDK 进来,网关先做协议解析,再决定字段怎么转。app/protocols/cross.py、cross_messages.py、cross_tools.py 管 Chat Completions、Responses、Anthropic Messages 三套协议。
协议矩阵
能转的字段:role/text、max_tokens ↔ max_completion_tokens/max_output_tokens、temperature/top_p、stop ↔ stop_sequences、function schema ↔ input_schema、tool_choice(required ↔ any)、parallel_tool_calls ↔ disable_parallel_tool_use、user ↔ metadata.user_id。不能转的直接 400 unsupported_parameter:reasoning/thinking、response_format/text.format 到 Anthropic、cache_control、service_tier、未知 metadata key。_output_limit() 连数值相同的重复 limit 字段都拒绝。结果是普通 SDK 带上可选字段时,要么被转换,要么明确失败,不会静默丢掉。
流式
流式不是转发上游。chat_completions.py 的 stream_frames() 在完整 ReplyDraft 生成后,把文本/推理/工具参数切成 200 字符的 frame;_stream() 每发一帧前重读任务状态。LLM 策略会先把上游 SSE 全部读完、落库,再伪流式输出;工具调用先发 name,再发参数字片。没有首字节收益,人等待时连接看起来是空闲的。预算:16MiB/600s 流、1MiB 单行 SSE、非流式 8MiB;Settings 里的 stream_chunk_size=24、stream_delay_min_ms=20、max=90 目前没有使用。
人工提交
正确性靠 SQLite 条件更新。tasks.py 的 first_reply_wins() 是 UPDATE ... WHERE state=WAITING_HUMAN AND version=expected_version,晚提交返回 False 和 REPLY_REJECTED_LATE/409;claim_fallback() 原子地把 WAITING_HUMAN 改成 FORWARDING_LLM;finalize(allowed_sources) 防止超时任务覆盖人工答案;slot_released_at IS NULL 让释放幂等。准入用 BEGIN IMMEDIATE + 每用户 10 个 active task(429),所有 key/策略共享。单写 SQLite,没有分布式锁。
人的超时是 10–1800 秒,默认 300,创建任务时快照 human_deadline_at。fallback 只 claim 一次,不重试;上游失败是 FAILED/500,不是 504;调用方只看到通用 Request timed out.。等待过程中没有进度信号,也不能按请求延长。
安全边界
SSRF 永远拦截 169.254.169.254、100.100.100.200、168.63.129.16、metadata.google.internal,默认拦 loopback/RFC1918/ULA/link-local,DNS 每次请求前重解析,GATEWAY_PUBLIC_HOSTS 防自指;密钥 hlg1.<version>.<nonce>.<ciphertext> 用 HKDF-SHA256 + AES-256-GCM;/v1/* 8MiB、/api/* 1MiB;高频记录 7 天。工具调用只校验调用方声明的名字和 JSON Schema,不执行、不沙箱。多 worker 明确不支持。