AGC 支持 Anthropic 独立网关路由与后台协议选项
新增平台 Anthropic 网关路由 /api/llm/anthropic/{*path},复用账号 Router 凭据边界
OpenAI 侧规范路径改为 /api/llm/v1/responses 与 /api/llm/v1/chat/completions,旧路径保留兼容
baseUrl 不再携带路由与版本段,统一由协议自己拼 v1/<op>
cc 执行器改走平台网关,ANTHROPIC_AUTH_TOKEN 复用平台会话,不再直连 Router
后台模型目录 agentMode 增加 anthropic,管理页与客户端契约同步
补齐三条路由埋点与静态段白名单,新增三条回归测试
同步后端架构文档、模型别名文档与排障记忆
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ use crate::{
|
||||
auth::require_bearer_auth,
|
||||
llm::{
|
||||
LLM_REQUEST_MAX_BODY_BYTES, list_llm_models, proxy_llm_chat_completions,
|
||||
proxy_llm_responses,
|
||||
proxy_llm_messages, proxy_llm_responses,
|
||||
},
|
||||
state::AppState,
|
||||
volcengine_speech::{
|
||||
@@ -36,6 +36,15 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
))
|
||||
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/llm/v1/chat/completions",
|
||||
post(proxy_llm_chat_completions)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
))
|
||||
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/llm/responses",
|
||||
post(proxy_llm_responses)
|
||||
@@ -45,6 +54,27 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
))
|
||||
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/llm/v1/responses",
|
||||
post(proxy_llm_responses)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
))
|
||||
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||
)
|
||||
// Anthropic 客户端(Claude Agent SDK / Claude Code CLI)固定请求
|
||||
// `{ANTHROPIC_BASE_URL}/v1/messages`,所以这里只声明 Anthropic 自己的
|
||||
// 路由前缀,尾段交给上游协议本身,网关不自己发明版本段。
|
||||
.route(
|
||||
"/api/llm/anthropic/{*path}",
|
||||
post(proxy_llm_messages)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
))
|
||||
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/speech/volcengine/config",
|
||||
get(get_volcengine_speech_config).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -602,6 +602,15 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option<RouteTrack
|
||||
Some(route_spec("llm_request", "llm", User, "anonymous"))
|
||||
}
|
||||
("POST", "/api/llm/responses") => Some(route_spec("llm_request", "llm", User, "anonymous")),
|
||||
("POST", "/api/llm/v1/responses") => {
|
||||
Some(route_spec("llm_request", "llm", User, "anonymous"))
|
||||
}
|
||||
("POST", "/api/llm/v1/chat/completions") => {
|
||||
Some(route_spec("llm_request", "llm", User, "anonymous"))
|
||||
}
|
||||
("POST", "/api/llm/anthropic/v1/messages") => {
|
||||
Some(route_spec("llm_request", "llm", User, "anonymous"))
|
||||
}
|
||||
("GET", "/api/speech/volcengine/config") => Some(route_spec(
|
||||
"speech_config_view",
|
||||
"speech",
|
||||
@@ -946,6 +955,7 @@ fn is_known_static_route_segment(segment: &str) -> bool {
|
||||
matches!(
|
||||
segment,
|
||||
"ai" | "analytics"
|
||||
| "anthropic"
|
||||
| "api"
|
||||
| "api-keys"
|
||||
| "asr"
|
||||
@@ -988,11 +998,13 @@ fn is_known_static_route_segment(segment: &str) -> bool {
|
||||
| "library"
|
||||
| "llm"
|
||||
| "login"
|
||||
| "messages"
|
||||
| "optimizations"
|
||||
| "login-options"
|
||||
| "logout"
|
||||
| "logout-all"
|
||||
| "me"
|
||||
| "responses"
|
||||
| "metric"
|
||||
| "objects"
|
||||
| "orders"
|
||||
@@ -1616,6 +1628,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_gateway_protocol_paths_keep_their_tracking_specs() {
|
||||
// 协议路径段必须保持静态,否则归一化会把 Anthropic 路由打成
|
||||
// `/api/llm/{id}/v1/{id}`,埋点永远匹配不上。
|
||||
assert_eq!(
|
||||
normalize_route_path("/api/llm/anthropic/v1/messages"),
|
||||
"/api/llm/anthropic/v1/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_route_path("/api/llm/v1/responses"),
|
||||
"/api/llm/v1/responses"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_route_path("/api/llm/v1/chat/completions"),
|
||||
"/api/llm/v1/chat/completions"
|
||||
);
|
||||
|
||||
for path in [
|
||||
"/api/llm/v1/responses",
|
||||
"/api/llm/v1/chat/completions",
|
||||
"/api/llm/anthropic/v1/messages",
|
||||
] {
|
||||
let spec = resolve_route_tracking_spec(&Method::POST, path)
|
||||
.unwrap_or_else(|| panic!("{path} should keep the llm_request spec"));
|
||||
assert_eq!(spec.event_key, "llm_request");
|
||||
assert_eq!(spec.module_key, "llm");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_runtime_settings_routes_keep_tracking_specs() {
|
||||
for (method, event_key) in [
|
||||
|
||||
@@ -17,6 +17,9 @@ pub const AGC_MODEL_CATALOG_MAX_MODELS: usize = 32;
|
||||
pub enum AgcAgentMode {
|
||||
Codex,
|
||||
Cc,
|
||||
/// 显式 Anthropic Messages 协议。`cc` 是同一执行器的历史别名,
|
||||
/// 新目录项应直接写 `anthropic`。
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
impl Default for AgcAgentMode {
|
||||
@@ -30,6 +33,7 @@ impl AgcAgentMode {
|
||||
match self {
|
||||
Self::Codex => "codex",
|
||||
Self::Cc => "cc",
|
||||
Self::Anthropic => "anthropic",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +41,8 @@ impl AgcAgentMode {
|
||||
match value {
|
||||
"codex" => Ok(Self::Codex),
|
||||
"cc" => Ok(Self::Cc),
|
||||
_ => Err("Agent 模式只允许 codex 或 cc".into()),
|
||||
"anthropic" => Ok(Self::Anthropic),
|
||||
_ => Err("Agent 模式只允许 codex、cc 或 anthropic".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,13 +345,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_agent_mode_accepts_codex_and_cc_only() {
|
||||
fn catalog_agent_mode_accepts_codex_cc_and_anthropic_only() {
|
||||
let mut catalog =
|
||||
AgcModelCatalog::from_upstream_models(upstream(&["model-a", "model-b"]), 0).unwrap();
|
||||
catalog.models[0].agent_mode = AgcAgentMode::Cc;
|
||||
assert!(catalog.validate().is_ok());
|
||||
assert_eq!(catalog.models[0].agent_mode.as_str(), "cc");
|
||||
|
||||
catalog.models[0].agent_mode = AgcAgentMode::Anthropic;
|
||||
assert!(catalog.validate().is_ok());
|
||||
assert_eq!(catalog.models[0].agent_mode.as_str(), "anthropic");
|
||||
assert_eq!(
|
||||
AgcAgentMode::parse("anthropic").unwrap(),
|
||||
AgcAgentMode::Anthropic
|
||||
);
|
||||
|
||||
let invalid: Result<AgcModelCatalog, _> = serde_json::from_str(
|
||||
r#"{"revision":0,"defaultModelId":"model-a","models":[{"id":"model-a","alias":"model-a","modelId":"model-a","enabled":true,"agentMode":"provider"}]}"#,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user