providers: add teamorouter; pick the first /models id when no default model

This commit is contained in:
WHC
2026-10-08 14:30:00 +08:00
parent 1772c26d43
commit 0226f417ff
6 changed files with 52 additions and 5 deletions
+1
View File
@@ -171,6 +171,7 @@ Gitea Release `vVERSION`(同名资源会被替换);仓库可设 `RELEASE_T
| `minimax`(默认) | `https://api.minimax.cn/v1` | `MiniMax-M2.7` | `MINIMAX_API_KEY` | | `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` | | `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` | | `openai` | `https://api.openai.com/v1` | `gpt-4.1-mini` | `OPENAI_API_KEY` |
| `teamorouter` | `https://api.teamorouter.com/v1` | 登录后自动取 `/models` 第一个(`/model` 可换) | `TEAMOROUTER_API_KEY` |
| 用途 | 来源(优先级从高到低) | | 用途 | 来源(优先级从高到低) |
|---|---| |---|---|
+1
View File
@@ -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` | | `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` | | `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` | | `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` / 下表中的环境变量优先于预设。 显式给出的 `--api-base` / `--model` / 下表中的环境变量优先于预设。
+16 -1
View File
@@ -20,7 +20,8 @@
static void usage(const char *argv0) { static void usage(const char *argv0) {
fprintf(stderr, fprintf(stderr,
"Usage: %s [options] (works in the current directory)\n" "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" " -m, --model NAME model (last used for the provider)\n"
" -e, --effort LEVEL low | medium | high | max (last used)\n" " -e, --effort LEVEL low | medium | high | max (last used)\n"
" -w, --workspace DIR project directory (default: current)\n" " -w, --workspace DIR project directory (default: current)\n"
@@ -539,6 +540,20 @@ int main(int argc, char **argv) {
curl_global_cleanup(); curl_global_cleanup();
return 1; 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, fprintf(stderr,
"using %s model=%s effort=%s protocol=%s tools=%s max_steps=%d\n", "using %s model=%s effort=%s protocol=%s tools=%s max_steps=%d\n",
+7
View File
@@ -11,6 +11,9 @@ static const cst_provider_t PROVIDERS[] = {
{"deepseek", "https://api.deepseek.com/v1", "deepseek-flash", {"deepseek", "https://api.deepseek.com/v1", "deepseek-flash",
"DEEPSEEK_API_KEY"}, "DEEPSEEK_API_KEY"},
{"openai", "https://api.openai.com/v1", "gpt-4.1-mini", "OPENAI_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) { 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}, {"o3", 200000}, {"o4", 200000}, {"claude", 200000},
{"qwen", 131072}, {"glm", 128000}, {"kimi", 131072}}; {"qwen", 131072}, {"glm", 128000}, {"kimi", 131072}};
if (model) { 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++) { for (size_t i = 0; i < sizeof(T) / sizeof(T[0]); i++) {
if (strncmp(model, T[i].prefix, strlen(T[i].prefix)) == 0) if (strncmp(model, T[i].prefix, strlen(T[i].prefix)) == 0)
return T[i].tokens; return T[i].tokens;
+21 -4
View File
@@ -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 * File-scope so a probe still running when the TUI exits writes nowhere
* stale. */ * 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 }; enum { AM_NONE = 0, AM_RUNNING, AM_OK, AM_FAILED, AM_AUTH };
static pthread_mutex_t g_am_mu = PTHREAD_MUTEX_INITIALIZER; 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; 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) { 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] = ""; char msg[400] = "";
int picked = 0;
pthread_mutex_lock(&g_am_mu); pthread_mutex_lock(&g_am_mu);
if (cfg && strcmp(g_am.provider, cfg->provider) == 0) { if (cfg && strcmp(g_am.provider, cfg->provider) == 0) {
if (g_am.state == AM_AUTH) { if (g_am.state == AM_AUTH) {
snprintf(msg, sizeof(msg), "the %s API key was rejected — /key to " snprintf(msg, sizeof(msg), "the %s API key was rejected — /key to "
"replace it", cfg->provider); "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) { } else if (g_am.state == AM_OK && g_am.n > 0) {
int found = 0; int found = 0;
for (int i = 0; i < g_am.n && !found; i++) 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); pthread_mutex_unlock(&g_am_mu);
if (picked)
cst_settings_remember(cfg);
if (msg[0]) if (msg[0])
set_status(t, msg); set_status(t, msg);
} }
@@ -4314,7 +4324,8 @@ static void cmd_provider(tui_t *t, char *arg, int *quit) {
int cur_idx = 0; int cur_idx = 0;
for (int i = 0; i < n && i < 16; i++) { for (int i = 0; i < n && i < 16; i++) {
const cst_provider_t *q = cst_provider_at(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) if (strcmp(q->name, cfg->provider) == 0)
cur_idx = i; cur_idx = i;
} }
@@ -5409,6 +5420,12 @@ static void request_cancel_ui(tui_t *t) {
static int begin_turn(tui_t *t, char *prompt) { static int begin_turn(tui_t *t, char *prompt) {
prompt_hist_push(t, 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); block_t *ub = blocks_push(&t->hist);
if (ub) { if (ub) {
ub->kind = BLK_USER; ub->kind = BLK_USER;
+6
View File
@@ -72,6 +72,12 @@ static int start_server(void) {
"Connection: close\r\n\r\n" body "Connection: close\r\n\r\n" body
int main(void) { 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\":[" g_responses[0] = RESP("200 OK", "{\"object\":\"list\",\"data\":["
"{\"id\":\"deepseek-flash\"}," "{\"id\":\"deepseek-flash\"},"
"{\"id\":\"deepseek-v4-pro\"}," "{\"id\":\"deepseek-v4-pro\"},"