Skip to content

Commit 05b672a

Browse files
committed
refactor: 重构工具选择逻辑与多项目路由,优化配置与测试
1. 重构tool_search工具调用的函数名获取逻辑,使用上下文方法获取注册后的名称 2. 完善main.rs的配置覆盖初始化,正确传递启动参数 3. 重构项目鉴权与路由逻辑,优化多项目下的token验证与模型路由匹配 4. 调整代码格式与换行,修复代码冗余空行 5. 新增并完善多项目相关测试用例,优化现有测试的可读性 6. 更新中文使用文档,修正说明描述与操作步骤
1 parent cc1ec47 commit 05b672a

12 files changed

Lines changed: 342 additions & 114 deletions

docs/USAGE.zh-CN.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Codex OpenCode Adapter 使用说明
1+
# Codex OpenCode Adapter 使用说明
22

33
这份说明用于安装、启动和排查薄 Bridge,目标是避免用真实模型反复试错。
44

@@ -13,7 +13,7 @@
1313
1. 全局安装(仅首次,或更新后重新安装):
1414

1515
```powershell
16-
cargo install codex-opencode-adapter
16+
cargo install --path .
1717
```
1818

1919
2. 初始化配置(安装用户级 Provider + 写入项目配置 + 写入默认 OSS agent 模板):
@@ -50,7 +50,7 @@ cargo run -- start
5050

5151
`check` 主要用于排障,`auth print-local-token` 主要给 provider auth helper 调用,不是日常手工入口。
5252

53-
> `auth print-local-token` 返回 adapter token,不再负责选择项目。项目选择只来自请求里的 `model = "opencode_adapter/<project_key>/<real_model>"``opencode-go/<model-id>` 已不再支持。
53+
> `auth print-local-token` 输出 adapter 本地 token,用于证明调用方可以访问本机 adapter;项目选择不由 token 决定,而由请求中的 `model = "opencode_adapter/<project_key>/<real_model>"` 决定。无项目上下文时可从 registry 中任一已注册项目读取本地 token,因为 token 是 adapter 级鉴权而不是项目路由。`opencode-go/<model-id>` 已不再支持。
5454
5555
## 1. 配置分层
5656

@@ -148,9 +148,11 @@ Invoke-RestMethod http://127.0.0.1:4010/health
148148
期望 `status``ok`
149149

150150
```powershell
151-
cargo test
151+
Invoke-RestMethod http://127.0.0.1:4010/v1/models -Headers @{Authorization = "Bearer $(codex-opencode-adapter auth print-local-token)"}
152152
```
153153

154+
期望返回 `object``list``data` 为可用模型列表。
155+
154156
以上任一步失败,都不要启动子代理。
155157

156158
## 5. 让 Desktop 加载配置

src/conversion/responses_to_chat.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,8 @@ fn convert_tool_choice(choice: Option<&Value>, context: &ToolContext) -> Option<
671671
);
672672
}
673673
if kind == "tool_search" {
674-
return Some(json!({"type":"function","function":{"name":"tool_search"}}));
674+
let chat_name = context.chat_name_for_response_function("tool_search", None);
675+
return Some(json!({"type":"function","function":{"name":chat_name}}));
675676
}
676677
Some(choice.clone())
677678
}

src/main.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clap::Parser;
1+
use clap::Parser;
22
use codex_opencode_adapter::cli::{AuthCommands, Cli, Commands, RunArgs};
33
use codex_opencode_adapter::config::{
44
Config, ConfigOverrides, DEFAULT_HOST, DEFAULT_MAX_CONCURRENCY, DEFAULT_PORT,
@@ -49,13 +49,13 @@ async fn run_server(args: RunArgs) -> anyhow::Result<()> {
4949
let config_overrides = ConfigOverrides {
5050
host: args.host.clone(),
5151
port: args.port,
52-
upstream_base: None,
53-
upstream_key: None,
54-
local_token: None,
55-
state_db: None,
56-
state_ttl_seconds: None,
57-
timeout_seconds: None,
58-
max_request_bytes: None,
52+
upstream_base: args.upstream_base.clone(),
53+
upstream_key: args.upstream_key.clone(),
54+
local_token: args.local_token.clone(),
55+
state_db: args.state_db.clone(),
56+
state_ttl_seconds: args.state_ttl_seconds,
57+
timeout_seconds: args.timeout_seconds,
58+
max_request_bytes: args.max_request_bytes,
5959
max_concurrency: args.max_concurrency,
6060
};
6161

src/project.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{anyhow, Context};
1+
use anyhow::{anyhow, Context};
22
use hmac::{Hmac, Mac};
33
use serde::{Deserialize, Serialize};
44
use serde_json::Value;
@@ -185,8 +185,6 @@ impl ProjectPaths {
185185
}
186186
Self::from_root(start.to_path_buf())
187187
}
188-
189-
190188
}
191189

192190
fn codex_thread_ids() -> Vec<String> {
@@ -317,8 +315,7 @@ pub fn resolve_project() -> anyhow::Result<ProjectPaths> {
317315
}
318316

319317
// Priority 2: cwd / ancestor walk
320-
let cwd = std::env::current_dir()
321-
.context("failed to resolve current working directory")?;
318+
let cwd = std::env::current_dir().context("failed to resolve current working directory")?;
322319
let discovered = ProjectPaths::discover_from(&cwd);
323320
if discovered.env_file.exists() {
324321
validate_recovered_project(&discovered)
@@ -335,8 +332,9 @@ pub fn resolve_project() -> anyhow::Result<ProjectPaths> {
335332
{
336333
let thread_paths = ProjectPaths::discover_from(&thread_cwd);
337334
if thread_paths.env_file.exists() {
338-
validate_recovered_project(&thread_paths)
339-
.context("found project via Codex thread context but registry check failed")?;
335+
validate_recovered_project(&thread_paths).context(
336+
"found project via Codex thread context but registry check failed",
337+
)?;
340338
return Ok(thread_paths);
341339
}
342340
}
@@ -345,11 +343,10 @@ pub fn resolve_project() -> anyhow::Result<ProjectPaths> {
345343

346344
// Priority 4: Constrained fallback -- single registered project only
347345
if registry.projects.is_empty() {
348-
return Err(anyhow!(
349-
concat!(
350-
"No OpenCode adapter projects found.\n", " Run 'codex-opencode-adapter init' from your project root to create one."
351-
)
352-
));
346+
return Err(anyhow!(concat!(
347+
"No OpenCode adapter projects found.\n",
348+
" Run 'codex-opencode-adapter init' from your project root to create one."
349+
)));
353350
}
354351
if registry.projects.len() == 1 {
355352
let (pid, entry) = registry.projects.iter().next().unwrap();

src/server.rs

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use axum::extract::State;
1+
use axum::extract::State;
22
use axum::http::{HeaderMap, StatusCode};
33
use axum::response::{IntoResponse, Response, Sse};
44
use axum::routing::{get, post};
@@ -22,8 +22,8 @@ use crate::media_guard::{
2222
unsupported_multimodal_error_message,
2323
};
2424
use crate::project::{
25-
current_environment, project_id_from_key, project_key_from_id, read_project_env,
26-
registry_dir_path, validate_adapter_token, ProjectRegistry, PROJECT_ENV_FILENAME,
25+
project_id_from_key, project_key_from_id, read_project_env, registry_dir_path,
26+
validate_adapter_token, ProjectRegistry, PROJECT_ENV_FILENAME,
2727
};
2828
use crate::state::{now_ts, StateStore};
2929
use crate::upstream::{
@@ -144,7 +144,7 @@ async fn responses(State(state): State<AppState>, headers: HeaderMap, body: Stri
144144
if let Err(response) = authorize_adapter(&projects, &headers) {
145145
return *response;
146146
}
147-
match projects.get(&project_id) {
147+
match project_runtime(&projects, &project_id) {
148148
Some(r) => r.clone(),
149149
None => {
150150
return error_response(
@@ -222,7 +222,7 @@ async fn complete_response(
222222
&message,
223223
);
224224
}
225-
let permit = match capacity.clone().try_acquire_owned() {
225+
let _permit = match capacity.clone().try_acquire_owned() {
226226
Ok(permit) => permit,
227227
Err(_) => {
228228
return error_response(
@@ -233,7 +233,7 @@ async fn complete_response(
233233
}
234234
};
235235
let upstream = runtime.client.chat(payload).await;
236-
drop(permit);
236+
drop(_permit);
237237
let upstream = match upstream {
238238
Ok(value) => value,
239239
Err(error) => {
@@ -346,10 +346,18 @@ async fn stream_response(
346346
);
347347
if let Err(error) = assembler.start() {
348348
tracing::error!(error = %error, "failed to emit initial stream lifecycle events");
349+
350+
let response =
351+
responses_failed_value(&body, &model_alias, "internal_error", &error.to_string());
352+
let event = json!({"type":"response.failed","response":response});
353+
let _ = tx.send(Ok(axum::response::sse::Event::default()
354+
.event("response.failed")
355+
.data(event.to_string())));
349356
let _ = tx.send(Ok(axum::response::sse::Event::default().data("[DONE]")));
357+
350358
return;
351359
}
352-
let permit = match capacity.clone().try_acquire_owned() {
360+
let _permit = match capacity.clone().try_acquire_owned() {
353361
Ok(permit) => permit,
354362
Err(_) => {
355363
if let Err(error) =
@@ -362,7 +370,7 @@ async fn stream_response(
362370
}
363371
};
364372
let upstream = runtime_for_task.client.chat_stream(payload).await;
365-
drop(permit);
373+
366374
match upstream {
367375
Ok(mut stream) => {
368376
let mut buffer = String::new();
@@ -576,10 +584,7 @@ fn previous_response(
576584
Ok(Some(previous))
577585
}
578586

579-
fn authorize_adapter(
580-
projects: &HashMap<String, ProjectRuntime>,
581-
headers: &HeaderMap,
582-
) -> Result<(), Box<Response>> {
587+
fn adapter_bearer_token(headers: &HeaderMap) -> Result<&str, Box<Response>> {
583588
let auth_header = headers
584589
.get("authorization")
585590
.and_then(|v| v.to_str().ok())
@@ -590,19 +595,48 @@ fn authorize_adapter(
590595
"Missing Authorization header. Provide a valid Bearer token.",
591596
))
592597
})?;
593-
let raw_token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
598+
auth_header.strip_prefix("Bearer ").ok_or_else(|| {
594599
Box::new(error_response(
595600
StatusCode::UNAUTHORIZED,
596601
"unauthorized",
597602
"Invalid Authorization format. Expected 'Bearer <token>'.",
598603
))
599-
})?;
600-
for runtime in projects.values() {
601-
if let Some(ref local_token) = runtime.config.local_token {
602-
if !local_token.is_empty() && validate_adapter_token(raw_token, local_token) {
603-
return Ok(());
604-
}
605-
}
604+
})
605+
}
606+
607+
fn project_runtime<'a>(
608+
projects: &'a HashMap<String, ProjectRuntime>,
609+
project_id: &str,
610+
) -> Option<&'a ProjectRuntime> {
611+
projects
612+
.get(project_id)
613+
.or_else(|| projects.get(project_key_from_id(project_id)))
614+
.or_else(|| {
615+
let canonical_id = project_id_from_key(project_id);
616+
projects.get(&canonical_id)
617+
})
618+
}
619+
620+
fn runtime_accepts_token(runtime: &ProjectRuntime, raw_token: &str) -> bool {
621+
runtime
622+
.config
623+
.local_token
624+
.as_ref()
625+
.is_some_and(|local_token| {
626+
!local_token.is_empty() && validate_adapter_token(raw_token, local_token)
627+
})
628+
}
629+
630+
fn authorize_adapter(
631+
projects: &HashMap<String, ProjectRuntime>,
632+
headers: &HeaderMap,
633+
) -> Result<(), Box<Response>> {
634+
let raw_token = adapter_bearer_token(headers)?;
635+
if projects
636+
.values()
637+
.any(|runtime| runtime_accepts_token(runtime, raw_token))
638+
{
639+
return Ok(());
606640
}
607641
Err(Box::new(error_response(
608642
StatusCode::UNAUTHORIZED,
@@ -611,10 +645,7 @@ fn authorize_adapter(
611645
)))
612646
}
613647

614-
async fn admin_refresh(
615-
State(state): State<AppState>,
616-
headers: HeaderMap,
617-
) -> Response {
648+
async fn admin_refresh(State(state): State<AppState>, headers: HeaderMap) -> Response {
618649
// Auth: accept any valid adapter bearer token.
619650
let auth_ok = {
620651
let projects = state.projects.read().unwrap();
@@ -657,16 +688,20 @@ async fn admin_refresh(
657688
continue;
658689
}
659690
};
660-
let env = current_environment();
661-
let config = match Config::from_sources(&project_env, &env, state.config_overrides.clone()) {
691+
let env = HashMap::new();
692+
let config = match Config::from_sources(&project_env, &env, state.config_overrides.clone())
693+
{
662694
Ok(c) => c,
663695
Err(e) => {
664696
tracing::warn!("refresh: bad config for {project_id}: {e}");
665697
continue;
666698
}
667699
};
668700
let state_db_path = root.join(&config.state_db);
669-
let store = match StateStore::new(state_db_path.display().to_string(), config.state_ttl_seconds) {
701+
let store = match StateStore::new(
702+
state_db_path.display().to_string(),
703+
config.state_ttl_seconds,
704+
) {
670705
Ok(s) => s,
671706
Err(e) => {
672707
tracing::warn!("refresh: cannot create state for {project_id}: {e}");
@@ -684,14 +719,20 @@ async fn admin_refresh(
684719
continue;
685720
}
686721
};
687-
projects.insert(project_id.clone(), ProjectRuntime { config, client, state: store });
722+
projects.insert(
723+
project_id.clone(),
724+
ProjectRuntime {
725+
config,
726+
client,
727+
state: store,
728+
},
729+
);
688730
added.push(project_id.clone());
689731
}
690732

691733
Json(json!({"status":"ok","added":added,"already_loaded":skipped})).into_response()
692734
}
693735

694-
695736
fn parse_routed_model(model: &str) -> Result<(String, String), &'static str> {
696737
let Some(rest) = model.strip_prefix("opencode_adapter/") else {
697738
return Err("model must use opencode_adapter/<project_key>/<real_model>. Run 'codex-opencode-adapter init' to refresh agent templates.");
@@ -863,6 +904,3 @@ fn upstream_error(error: UpstreamError) -> Response {
863904
}
864905
}
865906
}
866-
867-
868-

0 commit comments

Comments
 (0)