Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 30 additions & 32 deletions c/cagent.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ static void image_paste_wrapper(char **out, size_t *outlen) {
int main(int argc, char *argv[]) {
srand((unsigned int)time(NULL) ^ (unsigned int)getpid());
/* 默认值 */
const char *provider = util_env("BASH_AGENT_PROVIDER", "claude");
const char *provider = "claude";
const char *model = NULL;
const char *api_key = NULL;
const char *base_url = NULL;
Expand Down Expand Up @@ -169,44 +169,42 @@ int main(int argc, char *argv[]) {
char *effective_model = NULL;
char *effective_base_url = NULL;

/* 先解析当前 provider 的显式参数和环境变量;DeepSeek 回退后才设置模型与传输配置。 */
if (strcmp(provider, "claude") == 0) {
effective_api_key = util_strdup(api_key ? api_key : util_env("ANTHROPIC_API_KEY", ""));
effective_base_url = util_strdup(base_url ? base_url : util_env("ANTHROPIC_BASE_URL", ""));
effective_model = util_strdup(model ? model : (getenv("MODEL") && getenv("MODEL")[0] ? getenv("MODEL") : "claude-sonnet-4-20250514"));

/* DeepSeek 自动检测 */
if (!effective_api_key[0] && !effective_base_url[0]) {
const char *ds_key = getenv("DEEPSEEK_API_KEY");
if (ds_key && ds_key[0]) {
free(effective_api_key);
free(effective_base_url);
effective_api_key = util_strdup(ds_key);
effective_base_url = util_strdup("https://api.deepseek.com/anthropic");
if (!model) {
free(effective_model);
effective_model = util_strdup("deepseek-v4-flash");
}
}
}

if (!effective_api_key[0] && !effective_base_url[0]) {
fprintf(stderr, "Error: no API key. Set ANTHROPIC_API_KEY or use --api-key\n");
return 1;
}
effective_api_key = util_strdup(api_key && api_key[0] ? api_key : util_env("ANTHROPIC_API_KEY", ""));
effective_base_url = util_strdup(base_url && base_url[0] ? base_url : util_env("ANTHROPIC_BASE_URL", ""));
} else if (strcmp(provider, "openai") == 0) {
effective_api_key = util_strdup(api_key ? api_key : util_env("OPENAI_API_KEY", ""));
effective_base_url = util_strdup(base_url ? base_url : util_env("OPENAI_BASE_URL", ""));
effective_model = util_strdup(model ? model : (getenv("MODEL") && getenv("MODEL")[0] ? getenv("MODEL") : "gpt-4o"));

if (!effective_api_key[0] && !effective_base_url[0]) {
fprintf(stderr, "Error: no API key. Set OPENAI_API_KEY or use --api-key\n");
return 1;
}
effective_api_key = util_strdup(api_key && api_key[0] ? api_key : util_env("OPENAI_API_KEY", ""));
effective_base_url = util_strdup(base_url && base_url[0] ? base_url : util_env("OPENAI_BASE_URL", ""));
} else {
fprintf(stderr, "Error: unknown provider '%s' (use claude|openai)\n", provider);
return 1;
}

/* 仅当前 provider 没有显式配置时,自动回退至 DeepSeek。 */
if (!effective_api_key[0] && !effective_base_url[0]) {
const char *ds_key = getenv("DEEPSEEK_API_KEY");
if (ds_key && ds_key[0]) {
provider = "claude";
free(effective_api_key);
free(effective_base_url);
effective_api_key = util_strdup(ds_key);
effective_base_url = util_strdup("https://api.deepseek.com/anthropic");
if (!model || !model[0]) effective_model = util_strdup("deepseek-v4-flash");
}
}

if (!effective_api_key[0] && !effective_base_url[0]) {
fprintf(stderr, "Error: no API key. Set %s_API_KEY or use --api-key\n",
strcmp(provider, "openai") == 0 ? "OPENAI" : "ANTHROPIC");
return 1;
}

if (!effective_model) {
effective_model = util_strdup(model && model[0] ? model :
(strcmp(provider, "openai") == 0 ? "gpt-4o" : "claude-sonnet-4-20250514"));
}

/* 继续 session */
char *effective_session = util_strdup(session_id);
if ((do_continue || do_fork) && (!effective_session || !effective_session[0])) {
Expand Down
34 changes: 19 additions & 15 deletions go/cmd/goagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,38 +127,42 @@ Examples:
cfg.Model = model
}

// Provider 默认值
// 先解析当前 provider 的显式参数和环境变量;DeepSeek 回退后才设置模型与传输配置。
switch cfg.Provider {
case "claude":
if cfg.APIKey == "" {
cfg.APIKey = os.Getenv("ANTHROPIC_API_KEY")
}
// DeepSeek auto-detection: DEEPSEEK_API_KEY 且未显式配置其他 base URL
if cfg.APIKey == "" && cfg.BaseURL == "" &&
os.Getenv("DEEPSEEK_API_KEY") != "" &&
os.Getenv("ANTHROPIC_BASE_URL") == "" &&
os.Getenv("OPENAI_BASE_URL") == "" {
cfg.APIKey = os.Getenv("DEEPSEEK_API_KEY")
cfg.BaseURL = "https://api.deepseek.com/anthropic"
if cfg.Model == "" {
cfg.Model = "deepseek-v4-flash"
}
}
if cfg.BaseURL == "" {
cfg.BaseURL = os.Getenv("ANTHROPIC_BASE_URL")
}
if cfg.Model == "" {
cfg.Model = "claude-sonnet-4-20250514"
}
case "openai":
if cfg.APIKey == "" {
cfg.APIKey = os.Getenv("OPENAI_API_KEY")
}
if cfg.BaseURL == "" {
cfg.BaseURL = os.Getenv("OPENAI_BASE_URL")
}
default:
fmt.Fprintf(os.Stderr, "Unknown provider: %s (use claude|openai)\n", cfg.Provider)
os.Exit(1)
}

// 仅当前 provider 没有显式配置时,自动回退至 DeepSeek。
if cfg.APIKey == "" && cfg.BaseURL == "" && os.Getenv("DEEPSEEK_API_KEY") != "" {
cfg.Provider = "claude"
cfg.APIKey = os.Getenv("DEEPSEEK_API_KEY")
cfg.BaseURL = "https://api.deepseek.com/anthropic"
if cfg.Model == "" {
cfg.Model = "deepseek-v4-flash"
}
}

if cfg.Model == "" {
if cfg.Provider == "openai" {
cfg.Model = "gpt-4o"
} else {
cfg.Model = "claude-sonnet-4-20250514"
}
}

Expand Down
62 changes: 33 additions & 29 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,35 +198,15 @@ pub mod config {
}
}

// 先解析当前 provider 的显式参数和环境变量;DeepSeek 回退后才设置模型与传输配置。
match cfg.provider.as_str() {
"claude" => {
if cfg.api_key.is_empty() {
cfg.api_key = std::env::var("ANTHROPIC_API_KEY").unwrap_or_default();
}
// DeepSeek auto-detection
if cfg.api_key.is_empty()
&& cfg.base_url.is_empty()
&& std::env::var("DEEPSEEK_API_KEY")
.map(|v| !v.is_empty())
.unwrap_or(false)
&& std::env::var("ANTHROPIC_BASE_URL").is_err()
&& std::env::var("OPENAI_BASE_URL").is_err()
{
cfg.api_key = std::env::var("DEEPSEEK_API_KEY").unwrap();
cfg.base_url = "https://api.deepseek.com/anthropic".to_string();
if cfg.model.is_empty() {
cfg.model = "deepseek-v4-flash".to_string();
}
}
if cfg.base_url.is_empty() {
cfg.base_url = std::env::var("ANTHROPIC_BASE_URL").unwrap_or_default();
}
if cfg.model.is_empty() {
cfg.model = "claude-sonnet-4-20250514".to_string();
}
if cfg.api_key.is_empty() && cfg.base_url.is_empty() {
bail!("no API key. Set ANTHROPIC_API_KEY or use --api-key");
}
}
"openai" => {
if cfg.api_key.is_empty() {
Expand All @@ -235,15 +215,39 @@ pub mod config {
if cfg.base_url.is_empty() {
cfg.base_url = std::env::var("OPENAI_BASE_URL").unwrap_or_default();
}
if cfg.model.is_empty() {
cfg.model = "gpt-4o".to_string();
}
if cfg.api_key.is_empty() && cfg.base_url.is_empty() {
bail!("no API key. Set OPENAI_API_KEY or use --api-key");
}
}
_ => bail!("unknown provider: {} (use claude|openai)", cfg.provider),
}

// 仅当前 provider 没有显式配置时,自动回退至 DeepSeek。
if cfg.api_key.is_empty() && cfg.base_url.is_empty() {
if let Ok(ds_key) = std::env::var("DEEPSEEK_API_KEY") {
if !ds_key.is_empty() {
cfg.provider = "claude".to_string();
cfg.api_key = ds_key;
cfg.base_url = "https://api.deepseek.com/anthropic".to_string();
if cfg.model.is_empty() {
cfg.model = "deepseek-v4-flash".to_string();
}
}
}
}

if cfg.api_key.is_empty() && cfg.base_url.is_empty() {
match cfg.provider.as_str() {
"claude" => bail!("no API key. Set ANTHROPIC_API_KEY or use --api-key"),
"openai" => bail!("no API key. Set OPENAI_API_KEY or use --api-key"),
_ => unreachable!(),
}
}

if cfg.model.is_empty() {
cfg.model = match cfg.provider.as_str() {
"claude" => "claude-sonnet-4-20250514",
"openai" => "gpt-4o",
_ => unreachable!(),
}.to_string();
}
Ok(())
}

Expand All @@ -255,15 +259,15 @@ pub mod config {
} else {
cfg.base_url.as_str()
};
format!("{}/messages", base.trim_end_matches('/'))
format!("{}/messages", base)
}
"openai" => {
let base = if cfg.base_url.is_empty() {
"https://api.openai.com/v1"
} else {
cfg.base_url.as_str()
};
format!("{}/chat/completions", base.trim_end_matches('/'))
format!("{}/chat/completions", base)
}
_ => String::new(),
}
Expand Down
65 changes: 42 additions & 23 deletions src/agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -418,12 +418,20 @@ store_session_get_dir() {
}

store_session_get_latest_dir() {
local project_dir latest="" latest_ts=0 dir ts
local project_dir latest="" latest_ts=0 dir ts stat_cmd
project_dir="$(store_session_get_dir)"
[[ -d "$project_dir" ]] || return 1
if stat -c "%Y" "$project_dir" &>/dev/null; then
stat_cmd=(stat -c "%Y")
else
stat_cmd=(stat -f "%m")
fi
for dir in "$project_dir"/*/; do
[[ -d "$dir" ]] || continue
ts=$(stat -f "%m" "$dir/events.jsonl" 2>/dev/null || stat -c "%Y" "$dir/events.jsonl" 2>/dev/null || stat -f "%m" "$dir" 2>/dev/null || stat -c "%Y" "$dir" 2>/dev/null || echo 0)
local target="$dir"
[[ -f "$dir/events.jsonl" ]] && target="$dir/events.jsonl"
ts=$("${stat_cmd[@]}" "$target" 2>/dev/null || echo 0)
[[ "$ts" =~ ^[0-9]+$ ]] || ts=0
(( ts > latest_ts )) && { latest_ts=$ts; latest="$dir"; }
done
[[ -n "$latest" ]] && basename "$latest" || return 1
Expand Down Expand Up @@ -1626,10 +1634,41 @@ list_sessions() {
}

validate_config() {
# Resolve the selected provider's environment configuration first. Model defaults and
# transport settings are applied only after DeepSeek fallback selects the final provider.
case "$PROVIDER" in
claude)
: "${API_KEY:=$ANTHROPIC_API_KEY}"
: "${API_KEY:=${ANTHROPIC_API_KEY:-}}"
: "${BASE_URL:=${ANTHROPIC_BASE_URL:-}}"
;;
openai)
: "${API_KEY:=${OPENAI_API_KEY:-}}"
: "${BASE_URL:=${OPENAI_BASE_URL:-}}"
;;
*)
util_die "Unknown provider: $PROVIDER (use claude|openai)"
;;
esac

# Auto-detect DeepSeek only when the selected provider has no explicit configuration.
# MODEL is still empty here unless the user supplied -m/--model.
if [[ -z "$API_KEY" && -z "$BASE_URL" && -n "${DEEPSEEK_API_KEY:-}" ]]; then
PROVIDER="claude"
API_KEY="$DEEPSEEK_API_KEY"
BASE_URL="https://api.deepseek.com/anthropic"
: "${MODEL:=deepseek-v4-flash}"
fi

if [[ -z "$API_KEY" && -z "$BASE_URL" ]]; then
case "$PROVIDER" in
claude) util_die "No API key. Set ANTHROPIC_API_KEY or use --api-key" ;;
openai) util_die "No API key. Set OPENAI_API_KEY or use --api-key" ;;
esac
fi

# Build the final transport only after provider fallback has completed.
case "$PROVIDER" in
claude)
: "${MODEL:=claude-sonnet-4-20250514}"
API_URL="${BASE_URL:-https://api.anthropic.com/v1}/messages"
HEADER_ARGS=(
Expand All @@ -1643,8 +1682,6 @@ validate_config() {
sse_convert() { cat; }
;;
openai)
: "${API_KEY:=$OPENAI_API_KEY}"
: "${BASE_URL:=${OPENAI_BASE_URL:-}}"
: "${MODEL:=gpt-4o}"
API_URL="${BASE_URL:-https://api.openai.com/v1}/chat/completions"
HEADER_ARGS=(
Expand All @@ -1655,26 +1692,8 @@ validate_config() {
util_body_convert() { util_awk_run -f "$AWK_DIR/json.awk" -f "$AWK_DIR/transport_openai_body.awk"; }
sse_convert() { util_awk_run -f "$AWK_DIR/json.awk" -f "$AWK_DIR/transport_openai_sse.awk"; }
;;
*)
util_die "Unknown provider: $PROVIDER (use claude|openai)"
;;
esac

# Auto-detect DeepSeek API key: if DEEPSEEK_API_KEY is set, use DeepSeek's Anthropic endpoint
if [[ -z "$API_KEY" && -z "$BASE_URL" && -n "${DEEPSEEK_API_KEY:-}" ]]; then
PROVIDER="claude"
API_KEY="$DEEPSEEK_API_KEY"
BASE_URL="https://api.deepseek.com/anthropic"
: "${MODEL:=deepseek-v4-flash}"
fi

if [[ -z "$API_KEY" && -z "$BASE_URL" ]]; then
case "$PROVIDER" in
claude) util_die "No API key. Set ANTHROPIC_API_KEY or use --api-key" ;;
openai) util_die "No API key. Set OPENAI_API_KEY or use --api-key" ;;
esac
fi

# Unified SSE parser — same for all providers (sse_convert normalizes to Claude format)
sse_parse() { util_awk_run -v verbose="${VERBOSE:-false}" -f "$AWK_DIR/json.awk" -f "$AWK_DIR/protocol.awk" -f "$AWK_DIR/todo_protocol.awk" -f "$AWK_DIR/claude_sse.awk"; }
}
Expand Down
Loading