commit d140bf3b9088768c6532da38f50345ebb99d2378 from: Isaac Meerleo date: Tue Jul 21 15:22:16 2026 UTC Discover models below configured API prefixes commit - 013bbef42945efe028e39007e2c4b85909c42092 commit + d140bf3b9088768c6532da38f50345ebb99d2378 blob - 789d2e7baac27673436064c8a4bb921bb95b87c4 blob + d14ac8deda24ca7dec52894d25ddaf79726f713e --- docs/design/m7-curses.md +++ docs/design/m7-curses.md @@ -231,8 +231,11 @@ The remaining ยง2.1 surfaces landed in the M7 follow-u simply closes the drawer. - **The picker** (`/model`): fugu-api holds every configured provider's endpoint and key (PROV after HELLO) and fans a bounded - GET /v1/models across them; ids stream back tagged by provider - slot, relayed to the overlay (type-to-fuzzy-filter, Enter selects). + models GET across them. Direct Providers with a standard Generation-path + suffix preserve its configured prefix (`/api/v1/chat/completions` discovers + at `/api/v1/models`); unusual paths retain `/v1/models`. Ids stream back + tagged by provider slot, relayed to the overlay (type-to-fuzzy-filter, + Enter selects). Selection makes the provider active (SET_PROVIDER), swaps the model, cold-starts the cache, and re-sends UI_CONFIG. A provider that fails contributes nothing. The openai dialect's request codec blob - 3c7b71d199e9844692f20421304a55010fdd8b14 blob + 5e35973f8ce0f8d89e87a7f788867f2a3476e377 --- etc/fugu.conf.sample +++ etc/fugu.conf.sample @@ -48,8 +48,10 @@ # Direct-API endpoint overrides; the defaults follow the provider type # (api.anthropic.com or api.openai.com, port 443, /v1/messages or -# /v1/chat/completions). TLS only. Subscription endpoints are fixed and -# reject every endpoint override. +# /v1/chat/completions). For those standard suffixes, /model discovers at +# the sibling /models path under the same prefix; for example, +# /api/v1/chat/completions uses /api/v1/models. TLS only. Subscription +# endpoints are fixed and reject every endpoint override. #api_host "api.anthropic.com" #api_port 443 #api_path "/v1/messages" blob - 156955f32d3f03a8c55b72479053527f54b833fa blob + cf2924fc7f77e706f29f4f9754b28659901a1bc3 --- handoff/behavior.md +++ handoff/behavior.md @@ -667,7 +667,7 @@ Keywords (global or in `match` unless noted). Defaults | `ascii` yes\|no | ASCII-transliterated interface [no] | | `api_host` *string* | endpoint host [api.anthropic.com / api.openai.com] | | `api_port` *string* | endpoint port [443]; TLS only | -| `api_path` *string* | endpoint path [/v1/messages or /v1/chat/completions] | +| `api_path` *string* | endpoint path [/v1/messages or /v1/chat/completions]; standard suffixes preserve their prefix for sibling /models discovery | | `api_key` *string* | model API key (secret); `anthropic_key` is a legacy synonym for the anthropic provider, `api_key` wins if both | | `api_key_file` *string* | file whose first line is the key (secret; secrecy-checked); `anthropic_key_file` legacy synonym | | `oauth_token` *string* | personal subscription access token stored directly in the protected configuration; valid only for a `claude` or `codex` route | blob - 6ce6fbd248b4ec24efb550d290158c225e01152a blob + 4d2a2af3a02467f06c7b1e904286a8bfa3abb2ca --- regress/turn/run.sh +++ regress/turn/run.sh @@ -2250,6 +2250,30 @@ if [ -x "$obj/ptydrive" ]; then ok "picker: selected provider metadata drove the context window" $? stop_stub + # A compatible provider mounted below a path prefix discovers models + # beside its Chat Completions endpoint, not at the OpenAI default root. + rm -rf "$HOME/.fugu" + rm -f "$dir/reqout" + MODELS_PATH=/api/v1/models start_stub "OAI_OK OAI_OK" + cat > "$dir/fugu.conf" <"$dir/pickerout" 2>&1 + rc=$? + ok "prefixed picker: clean exit" $([ "$rc" -eq 0 ]; echo $?) + grep -q '"model":"stub-model-b"' "$dir/reqout" 2>/dev/null + ok "prefixed picker: models path follows the configured API prefix" $? + stop_stub + # Request a listing and immediately submit a slow-tool prompt; delayed # selection must land between its two Generations. The selection cools # the Projection cursor but must not terminate the Turn transaction. blob - 45c12b71a2088f40f83ca91b6dd43a40fc6122f5 blob + 62d4ec7d88e83b7cf6a0f8b16c2a92d83638fba6 --- regress/turn/stub.c +++ regress/turn/stub.c @@ -1563,7 +1563,7 @@ main(int argc, char *argv[]) socklen_t slen; const char *cert = NULL, *key = NULL, *keyout, *reqout, *reqlog, *overlapout, *poolout, - *weborderout, *futurepath; + *weborderout, *futurepath, *models_path; char *req; char web_first = '?', web_second = '?'; ssize_t reqlen; @@ -1594,6 +1594,9 @@ main(int argc, char *argv[]) poolout = getenv("POOLOUT"); weborderout = getenv("WEBORDEROUT"); futurepath = getenv("FUTUREPATH"); + models_path = getenv("MODELS_PATH"); + if (models_path == NULL || models_path[0] == '\0') + models_path = "/v1/models"; if ((req = calloc(1, STUB_REQ_MAX)) == NULL) errx(1, "request fixture allocation"); @@ -1692,7 +1695,10 @@ main(int argc, char *argv[]) i--; /* web origin call consumes no API scenario */ continue; } - if (strncmp(req, "GET /v1/models", 14) == 0) { + if (strncmp(req, "GET ", 4) == 0 && + strncmp(req + 4, models_path, + strlen(models_path)) == 0 && + req[4 + strlen(models_path)] == ' ') { /* the picker's live listing (behavior.md * 2.1): fixed ids, no scenario consumed */ static const char models[] = blob - c67bb9e8f4d39179fe5b07178ccb7244c97a758c blob + 7ba1ba3fb769cb0011587f58c6cb41eef96caec0 --- src/fugu/fugu.1 +++ src/fugu/fugu.1 @@ -335,6 +335,8 @@ Automatic Compaction uses the same operation; see .Xr fugu.conf 5 . .It Cm /model Open the curses model picker. +For direct Providers, discovery preserves the configured API path prefix when +the Generation endpoint uses the protocol's standard suffix. .It Cm /context Print byte-derived token estimates for the current projection and its largest items. blob - 0db78dfda50a0163b1410ae6bc79db67623c31b5 blob + 6350f5ca3ebe2d68545ba631deb2ee9b08848349 --- src/fugu/fugu.conf.5 +++ src/fugu/fugu.conf.5 @@ -256,6 +256,21 @@ and .Pa api.openai.com for .Cm openai . +When the configured path ends in the Provider protocol's standard +.Pa /messages +or +.Pa /chat/completions +suffix, model discovery preserves the preceding path prefix and uses its +sibling +.Pa /models +path. +For example, +.Pa /api/v1/chat/completions +discovers models from +.Pa /api/v1/models . +An unusual path with neither standard suffix retains the historical +.Pa /v1/models +discovery path. This directive is rejected when the implicit Provider is .Cm claude or @@ -667,7 +682,8 @@ or .Cm codex . .It Cm api_path Ar path Set the request path, with the same per-type defaults as the implicit -provider. +provider, including prefix-preserving model discovery for a standard request +suffix. This directive is rejected for .Cm claude or blob - f598c1cedcccf122a2df503ecf106cd991ba3377 blob + f54edc40b4d0cf78e89aa4be449a2794301b3bdd --- src/fugu-api/main.c +++ src/fugu-api/main.c @@ -1959,6 +1959,43 @@ request_new(struct worker *w, uint32_t id) } /* + * Direct Providers commonly mount their whole API below a path prefix. + * Preserve that prefix when moving from the typed Generation endpoint to + * model discovery. An unusual endpoint that does not use the protocol's + * standard suffix retains the historical fallback. + */ +static int +provider_models_path(const struct prov *v, char *out, size_t outsz) +{ + const char *suffix; + size_t pathlen, prefixlen, suffixlen; + + switch (v->type) { + case PROVIDER_OPENAI: + suffix = "/chat/completions"; + break; + case PROVIDER_ANTHROPIC: + suffix = "/messages"; + break; + default: + return (-1); + } + pathlen = strcspn(v->path, "?"); + suffixlen = strlen(suffix); + if (pathlen >= suffixlen && + memcmp(v->path + pathlen - suffixlen, suffix, suffixlen) == 0) { + prefixlen = pathlen - suffixlen; + if (outsz < sizeof("/models") || + prefixlen > outsz - sizeof("/models")) + return (-1); + memcpy(out, v->path, prefixlen); + (void)strlcpy(out + prefixlen, "/models", outsz - prefixlen); + return (0); + } + return (strlcpy(out, "/v1/models", outsz) >= outsz ? -1 : 0); +} + +/* * One provider's model listing (behavior.md 2.1, 8): GET the per-type * models endpoint and stream each id back tagged with the slot. The * body is hostile (T3): size-capped, ids bounded and NUL-free, at @@ -1972,6 +2009,7 @@ list_one(struct worker *w, struct api *a, int slot) struct buf body; struct json j; char headers[API_AUTH_HDR_MAX]; + char models_path[FUGU_API_PATH_MAX + 1]; char tmp[8192]; ssize_t n; int data, i, root, nmodels, rc, sent = 0; @@ -2000,6 +2038,8 @@ list_one(struct worker *w, struct api *a, int slot) buf_free(&e); return; } + if (provider_models_path(v, models_path, sizeof(models_path)) == -1) + return; { struct api_request fake; @@ -2017,7 +2057,7 @@ list_one(struct worker *w, struct api *a, int slot) http_close(&h); return; } - rc = http_request(&h, "GET", "/v1/models", headers, NULL, 0, + rc = http_request(&h, "GET", models_path, headers, NULL, 0, LIST_TIMEOUT); explicit_bzero(headers, sizeof(headers)); if (rc == -1 || http_response(&h, LIST_TIMEOUT) == -1 ||