AGC 官方 LLM Router 账号链路与流式联网输出 #242

Merged
kdletters merged 59 commits from feat/agc-llm-router-official-chain into master 2026-09-06 16:49:13 +08:00
2 changed files with 83 additions and 15 deletions
Showing only changes of commit 87c0cbd06d - Show all commits
@@ -1922,19 +1922,25 @@ impl CodexAppServerConnection {
"创建 Codex app-server 临时目录失败:{error}"
))
})?;
let direct_provider_route = match credential {
let (direct_provider_route, main_site_upstream) = match credential {
CodexAppServerCredential::PlatformSession {
api_base_url,
access_token,
..
} => Some((
format!("{}/api/llm", api_base_url.trim_end_matches('/')),
access_token.clone(),
)),
_ => (workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
.then(|| credential.direct_provider_route(llm))
.flatten()
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
} => (
Some((
format!("{}/api/llm", api_base_url.trim_end_matches('/')),
access_token.clone(),
)),
true,
),
_ => (
(workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
.then(|| credential.direct_provider_route(llm))
.flatten()
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
false,
),
};
let remote_control_disable_reason =
credential.remote_control_disable_reason(direct_provider_route.is_some());
@@ -2024,7 +2030,7 @@ impl CodexAppServerConnection {
};
let provider_proxy = if let Some((base_url, api_key)) = direct_provider_route.as_ref() {
Some(
start_codex_provider_proxy(base_url, api_key)
start_codex_provider_proxy(base_url, api_key, main_site_upstream)
.await
.map_err(platform_llm::LlmError::InvalidConfig)?,
)
@@ -4966,7 +4972,7 @@ mod tests {
#[tokio::test]
async fn direct_project_command_receives_only_provider_proxy_session_token() {
let provider_key = "fixture-upstream-provider-secret";
let proxy = start_codex_provider_proxy("http://127.0.0.1:9", provider_key)
let proxy = start_codex_provider_proxy("http://127.0.0.1:9", provider_key, false)
.await
.expect("start credential broker");
let mut command = tokio::process::Command::new("fixture");
@@ -9,12 +9,15 @@ use std::sync::Arc;
pub(crate) const CODEX_PROVIDER_PROXY_PROTOCOL: &str = "genarrative-codex-provider-proxy.v1";
const CODEX_PROVIDER_PROXY_MAX_REQUEST_BYTES: usize = 32 * 1024 * 1024;
const AGC_CLIENT_MARKER_HEADER: &str = "x-genarrative-client";
const AGC_CLIENT_MARKER_VALUE: &str = "agc";
#[derive(Clone)]
struct CodexProviderProxyState {
upstream_base_url: String,
upstream_bearer_token: String,
downstream_bearer_token: String,
main_site_upstream: bool,
client: reqwest::Client,
}
@@ -137,10 +140,14 @@ async fn proxy_codex_provider_request(
headers.append(name.clone(), value.clone());
}
}
headers.insert(
axum::http::HeaderName::from_static("x-genarrative-client"),
axum::http::HeaderValue::from_static("agc"),
);
// 仅 AGC 主站 `/api/llm` 路由需要携带保留的客户端标记,供服务端校验模型
// 方案;通用 Provider/凭据桥接不得把该标记外发给第三方上游。
if state.main_site_upstream {
headers.insert(
axum::http::HeaderName::from_static(AGC_CLIENT_MARKER_HEADER),
axum::http::HeaderValue::from_static(AGC_CLIENT_MARKER_VALUE),
);
}
let upstream_authorization = match format!("Bearer {}", state.upstream_bearer_token).parse() {
Ok(value) => value,
Err(_) => {
@@ -193,6 +200,7 @@ async fn proxy_codex_provider_request(
pub(crate) async fn start_codex_provider_proxy(
upstream_base_url: &str,
upstream_bearer_token: &str,
main_site_upstream: bool,
) -> Result<CodexProviderProxy, String> {
let upstream_base_url = normalize_codex_provider_upstream(upstream_base_url)?;
let upstream_bearer_token = upstream_bearer_token.trim();
@@ -223,6 +231,7 @@ pub(crate) async fn start_codex_provider_proxy(
upstream_base_url,
upstream_bearer_token: upstream_bearer_token.to_string(),
downstream_bearer_token: downstream_bearer_token.clone(),
main_site_upstream,
client,
});
let app = Router::new()
@@ -269,6 +278,25 @@ mod tests {
.expect("fake response")
}
async fn fake_main_site_upstream(
State(calls): State<Arc<AtomicUsize>>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Response<Body> {
calls.fetch_add(1, Ordering::SeqCst);
assert_eq!(
headers
.get(AGC_CLIENT_MARKER_HEADER)
.and_then(|value| value.to_str().ok()),
Some(AGC_CLIENT_MARKER_VALUE)
);
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(body))
.expect("fake main-site response")
}
#[tokio::test]
async fn loopback_proxy_strips_false_codex_limit_headers_and_requires_bearer() {
let calls = Arc::new(AtomicUsize::new(0));
@@ -285,6 +313,7 @@ mod tests {
let proxy = start_codex_provider_proxy(
&format!("http://127.0.0.1:{}", address.port()),
"fixture-provider-key",
false,
)
.await
.expect("start provider proxy");
@@ -328,4 +357,37 @@ mod tests {
assert_eq!(calls.load(Ordering::SeqCst), 1);
upstream_task.abort();
}
#[tokio::test]
async fn loopback_proxy_adds_main_site_marker_only_when_bridging_the_agc_route() {
let calls = Arc::new(AtomicUsize::new(0));
let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
.await
.expect("bind upstream");
let address = listener.local_addr().expect("upstream address");
let app = Router::new()
.route("/responses", post(fake_main_site_upstream))
.with_state(Arc::clone(&calls));
let upstream_task = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
let proxy = start_codex_provider_proxy(
&format!("http://127.0.0.1:{}", address.port()),
"fixture-provider-key",
true,
)
.await
.expect("start main-site provider proxy");
let accepted = reqwest::Client::new()
.post(format!("{}/responses", proxy.base_url()))
.bearer_auth(proxy.downstream_bearer_token())
.body("{\"input\":\"ok\"}")
.send()
.await
.expect("accepted main-site response");
assert_eq!(accepted.status(), StatusCode::OK);
assert_eq!(calls.load(Ordering::SeqCst), 1);
upstream_task.abort();
}
}