commit - 013bbef42945efe028e39007e2c4b85909c42092
commit + d140bf3b9088768c6532da38f50345ebb99d2378
blob - 789d2e7baac27673436064c8a4bb921bb95b87c4
blob + d14ac8deda24ca7dec52894d25ddaf79726f713e
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
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
# 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
| `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
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" <<EOF
+model "openrouter/default"
+provider "openai"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/api/v1/chat/completions"
+api_key "$KEY"
+EOF
+ printf '/model\r\000\000model-b\r\000hello\r\000/quit\r' | \
+ TERM=vt100 HOME=$HOME \
+ FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
+ FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=50 \
+ "$obj/ptydrive" "$fugu" >"$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
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;
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");
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
.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
.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
.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
}
/*
+ * 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
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;
buf_free(&e);
return;
}
+ if (provider_models_path(v, models_path, sizeof(models_path)) == -1)
+ return;
{
struct api_request fake;
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 ||