diff --git a/README.md b/README.md index 7e020a8..8b006d4 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ Gitea Release `vVERSION`(同名资源会被替换);仓库可设 `RELEASE_T | `minimax`(默认) | `https://api.minimax.cn/v1` | `MiniMax-M2.7` | `MINIMAX_API_KEY` | | `deepseek` | `https://api.deepseek.com/v1` | `deepseek-flash`(另有 `deepseek-v4-pro`) | `DEEPSEEK_API_KEY` | | `openai` | `https://api.openai.com/v1` | `gpt-4.1-mini` | `OPENAI_API_KEY` | +| `teamorouter` | `https://api.teamorouter.com/v1` | 登录后自动取 `/models` 第一个(`/model` 可换) | `TEAMOROUTER_API_KEY` | | 用途 | 来源(优先级从高到低) | |---|---| diff --git a/docs/env_openai.md b/docs/env_openai.md index d3d1c0a..f4a8d3f 100644 --- a/docs/env_openai.md +++ b/docs/env_openai.md @@ -13,6 +13,7 @@ CST-Agent 使用 **OpenAI Chat Completions** 协议(`POST {base}/chat/completi | `minimax`(默认) | `https://api.minimax.cn/v1` | `MiniMax-M2.7` | `MINIMAX_API_KEY` | | `deepseek` | `https://api.deepseek.com/v1` | `deepseek-flash`(另有 `deepseek-v4-pro`) | `DEEPSEEK_API_KEY` | | `openai` | `https://api.openai.com/v1` | `gpt-4.1-mini` | `OPENAI_API_KEY` | +| `teamorouter` | `https://api.teamorouter.com/v1` | 登录后自动取 `/models` 第一个(`/model` 可换) | `TEAMOROUTER_API_KEY` | 显式给出的 `--api-base` / `--model` / 下表中的环境变量优先于预设。 diff --git a/src/main.c b/src/main.c index 39c15d3..a07b140 100644 --- a/src/main.c +++ b/src/main.c @@ -20,7 +20,8 @@ static void usage(const char *argv0) { fprintf(stderr, "Usage: %s [options] (works in the current directory)\n" - " -P, --provider NAME minimax | deepseek | openai (last used)\n" + " -P, --provider NAME minimax | deepseek | openai | teamorouter\n" + " (last used)\n" " -m, --model NAME model (last used for the provider)\n" " -e, --effort LEVEL low | medium | high | max (last used)\n" " -w, --workspace DIR project directory (default: current)\n" @@ -539,6 +540,20 @@ int main(int argc, char **argv) { curl_global_cleanup(); return 1; } + /* Router presets have no fixed model: take the first one the API lists + * (the TUI does the same after its /models probe). */ + if (!cfg.model[0] && cfg.headless) { + char ids[1][128], err[256] = ""; + if (cst_llm_list_models(cfg.api_base, cfg.api_key, ids, 1, err, + sizeof(err)) > 0) { + snprintf(cfg.model, sizeof(cfg.model), "%s", ids[0]); + } else { + fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider, + err[0] ? err : "the API listed none"); + curl_global_cleanup(); + return 1; + } + } fprintf(stderr, "using %s model=%s effort=%s protocol=%s tools=%s max_steps=%d\n", diff --git a/src/models.c b/src/models.c index 2093dd2..95cb433 100644 --- a/src/models.c +++ b/src/models.c @@ -11,6 +11,9 @@ static const cst_provider_t PROVIDERS[] = { {"deepseek", "https://api.deepseek.com/v1", "deepseek-flash", "DEEPSEEK_API_KEY"}, {"openai", "https://api.openai.com/v1", "gpt-4.1-mini", "OPENAI_API_KEY"}, + /* Router: no fixed model; the first id from GET /models is picked. */ + {"teamorouter", "https://api.teamorouter.com/v1", "", + "TEAMOROUTER_API_KEY"}, }; const cst_provider_t *cst_provider_find(const char *name) { @@ -102,6 +105,10 @@ long cst_model_context_tokens(const char *model) { {"o3", 200000}, {"o4", 200000}, {"claude", 200000}, {"qwen", 131072}, {"glm", 128000}, {"kimi", 131072}}; if (model) { + /* routers name models "vendor/model" */ + const char *slash = strrchr(model, '/'); + if (slash && slash[1]) + model = slash + 1; for (size_t i = 0; i < sizeof(T) / sizeof(T[0]); i++) { if (strncmp(model, T[i].prefix, strlen(T[i].prefix)) == 0) return T[i].tokens; diff --git a/src/tui.c b/src/tui.c index ae63e70..8d2dc11 100644 --- a/src/tui.c +++ b/src/tui.c @@ -3821,7 +3821,7 @@ static int secret_input_run(tui_t *t, const char *title, const char *hint, * File-scope so a probe still running when the TUI exits writes nowhere * stale. */ -#define AM_MAX 64 +#define AM_MAX 512 /* routers list hundreds; the picker filters as you type */ enum { AM_NONE = 0, AM_RUNNING, AM_OK, AM_FAILED, AM_AUTH }; static pthread_mutex_t g_am_mu = PTHREAD_MUTEX_INITIALIZER; @@ -3920,15 +3920,23 @@ static int am_models(const char *provider, char out[][128], int max) { return n; } -/* After a probe: flag a model the API does not list. */ +/* After a probe: flag a model the API does not list; with no model yet + * (router presets) take the first one listed. */ static void am_check_model(tui_t *t) { - const cst_agent_config_t *cfg = cst_agent_config(t->agent); + cst_agent_config_t *cfg = cst_agent_config_mut(t->agent); char msg[400] = ""; + int picked = 0; pthread_mutex_lock(&g_am_mu); if (cfg && strcmp(g_am.provider, cfg->provider) == 0) { if (g_am.state == AM_AUTH) { snprintf(msg, sizeof(msg), "the %s API key was rejected — /key to " "replace it", cfg->provider); + } else if (g_am.state == AM_OK && g_am.n > 0 && !cfg->model[0]) { + snprintf(cfg->model, sizeof(cfg->model), "%s", g_am.ids[0]); + snprintf(msg, sizeof(msg), "model → %.127s (%d listed by %s) — /model " + "to change", + cfg->model, g_am.n, cfg->provider); + picked = 1; } else if (g_am.state == AM_OK && g_am.n > 0) { int found = 0; for (int i = 0; i < g_am.n && !found; i++) @@ -3939,6 +3947,8 @@ static void am_check_model(tui_t *t) { } } pthread_mutex_unlock(&g_am_mu); + if (picked) + cst_settings_remember(cfg); if (msg[0]) set_status(t, msg); } @@ -4314,7 +4324,8 @@ static void cmd_provider(tui_t *t, char *arg, int *quit) { int cur_idx = 0; for (int i = 0; i < n && i < 16; i++) { const cst_provider_t *q = cst_provider_at(i); - snprintf(items[i], 128, "%-9s %s (%s)", q->name, q->api_base, q->model); + snprintf(items[i], 128, "%-11s %s (%s)", q->name, q->api_base, + q->model[0] ? q->model : "models from API"); if (strcmp(q->name, cfg->provider) == 0) cur_idx = i; } @@ -5409,6 +5420,12 @@ static void request_cancel_ui(tui_t *t) { static int begin_turn(tui_t *t, char *prompt) { prompt_hist_push(t, prompt); + if (!cst_agent_config(t->agent)->model[0]) { + set_status(t, "no model selected yet — /model to pick one (↑ recalls " + "your message)"); + free(prompt); + return -1; + } block_t *ub = blocks_push(&t->hist); if (ub) { ub->kind = BLK_USER; diff --git a/tests/test_models.c b/tests/test_models.c index 00c368c..c2052cb 100644 --- a/tests/test_models.c +++ b/tests/test_models.c @@ -72,6 +72,12 @@ static int start_server(void) { "Connection: close\r\n\r\n" body int main(void) { + const cst_provider_t *tr = cst_provider_find("teamorouter"); + CHECK(tr && strcmp(tr->api_base, "https://api.teamorouter.com/v1") == 0); + CHECK(tr->model[0] == 0); /* picked from GET /models */ + CHECK(cst_model_context_tokens("anthropic/claude-sonnet-4") == 200000); + CHECK(cst_model_context_tokens("deepseek-flash") == 128000); + g_responses[0] = RESP("200 OK", "{\"object\":\"list\",\"data\":[" "{\"id\":\"deepseek-flash\"}," "{\"id\":\"deepseek-v4-pro\"},"