docs+test: custom providers in README and AGENTS.md, provider-only startup PTY test
Also records the codex catalog at eight models and the 0.159.0 User-Agent.
This commit is contained in:
1 parent
ae40ba8da2
commit
c4ef8a3779
3 files changed
+98
-6
No files matched your search
@@ -415,15 +415,17 @@ client-side, no backend surface.
|
||||
carries the thinking level. NO websocket, NO base_instructions.
|
||||
CATALOG is HARDCODED (`PlatformId::hardcoded_catalog` →
|
||||
`openai_codex_wire_models`, mapped through the SAME
|
||||
`platform_wire_model_to_entry` output): the 7 `visibility=list` &&
|
||||
`supported_in_api=true` models the backend served on 2026-09-22
|
||||
(`gpt-6-astra/sol/luna`, `gpt-5.6-sol/terra/luna`, `gpt-5.5`, ctx 272000,
|
||||
per-model efforts) — NO live `/models` fetch, NO codex-CLI / `~/.codex`
|
||||
`platform_wire_model_to_entry` output): the 8 `visibility=list` &&
|
||||
`supported_in_api=true` models the backend served on 2026-09-29
|
||||
(`gpt-6.1-sol`, `gpt-6-astra/sol/luna`, `gpt-5.6-sol/terra/luna`,
|
||||
`gpt-5.5`, ctx 272000, per-model efforts) — NO live `/models` fetch, NO codex-CLI / `~/.codex`
|
||||
dependency; the hidden `gpt-reserve` and `codex-auto-review` are
|
||||
EXCLUDED. Refresh the table by reading
|
||||
`GET {base}/models?client_version=<a current codex release>` with a
|
||||
ChatGPT bearer: an old `client_version` is answered with a truncated
|
||||
list, so a stale version reads as "the model does not exist".
|
||||
`CODEX_USER_AGENT` (`codex_cli_rs/0.159.0`) moves with the release used
|
||||
for that read.
|
||||
- `OAuthFlow::GithubDeviceCopilot` → `auth::github_copilot` (TWO-STAGE).
|
||||
Provider: `github-copilot` (`scope_key oauth/github-copilot`, base
|
||||
`api.individual.githubcopilot.com`, ChatCompletions wire). Stage 1 is an
|
||||
@@ -457,6 +459,39 @@ client-side, no backend surface.
|
||||
token (`resolve_generic_oauth_tokens`, refreshed on expiry) and routes
|
||||
`platform.oauth().is_some()` → `platform.base_url()` (kimi-code alone →
|
||||
`proxy_url()`). Tokens/codes/verifiers are NEVER logged.
|
||||
- CUSTOM PROVIDERS (0.1.19): `[platforms.<name>]` with `base_url` and `api =
|
||||
"openai" | "anthropic"` declares a provider outside the closed registry.
|
||||
`kigi-models/src/custom.rs` owns the validated type (name `[a-z0-9][a-z0-9_-]{0,31}`,
|
||||
never a registry id; base URL `http(s)://host[/path]`, no userinfo, query or
|
||||
fragment); `kigi-shell/src/agent/custom_providers/` owns the rest. Key order:
|
||||
auth.json scope `<name>`, then the table's `api_key`; a key is REQUIRED (a
|
||||
keyless declaration is inert). Wires: openai = `/chat/completions`, Bearer,
|
||||
`GET {base}/models`; anthropic = `/messages`, `x-api-key`,
|
||||
`GET {base}/models?limit=1000`. Catalog keys are `<name>/<model>`; the entry's
|
||||
`description` carries the provider name for the picker. Entries reach the disk
|
||||
cache without a key; `stamp_credentials` stamps `api_key` in memory, only onto
|
||||
entries still on the provider's own host, and `drop_orphans` removes fetched
|
||||
entries of a provider that is gone or keyless BEFORE the prefetched map replaces
|
||||
the catalog (a stale unkeyed entry would fall through to the house key).
|
||||
CREDENTIAL RULE: `EndpointsConfig.custom_provider_bases` (derived from
|
||||
`[platforms.*]` in `Config::new_from_toml_cfg` and `from_config_value`, never
|
||||
serialized) makes `CredentialAuthority::is_session_coding_endpoint` false for a
|
||||
declared base, loopback included — without it an Ollama-style provider on
|
||||
localhost classifies `Primary` and the Kimi bearer replaces its key. Every
|
||||
authority is built from the models manager's config, so the field reaches all of
|
||||
them. Turns and subagents read `auth_scheme` and BYOK status from the LIVE
|
||||
catalog entry (`auth_facts_of`), not from a disk reload: fetched entries never
|
||||
live on disk, and the reload sent fetched Messages models out with `Bearer`
|
||||
instead of `x-api-key` (this also repaired `anthropic` and `minimax`).
|
||||
Login: picker rows `custom-openai` / `custom-anthropic` are not loginable
|
||||
(`AuthMethodKind::Unknown`); the TUI asks base URL, name (default from the
|
||||
host), key, then `save_custom_provider` writes config.toml (through
|
||||
`config_write_dest`) and auth.json, and `authenticate("custom:<name>")`
|
||||
validates `GET {base}/models` (401/403 = rejected key, other non-2xx and network
|
||||
errors surface). Limits: context window 256K unless the listing serves
|
||||
`context_length` (Anthropic: `max_input_tokens`); no models.dev enrichment; a
|
||||
failed key check leaves the saved definition (login again with the same name to
|
||||
overwrite); no delete UI, edit config.toml and auth.json.
|
||||
- A stored subscription-OAuth session IS a catalog fetch source. Every
|
||||
fetch-PLAN decision that cannot afford real token resolution — the startup
|
||||
prefetch arming gate, `on_auth_changed`'s wipe guard
|
||||
|
||||
@@ -86,8 +86,8 @@ On by default. `KIGI_GRAPH=0` turns it off; `KIGI_GRAPH_CONCURRENCY` (default
|
||||
|
||||
## Providers and API keys
|
||||
|
||||
29 platforms ship compiled in: 5 you sign into, 24 you hand a key. Nothing is
|
||||
registered at runtime — if it's not in this list, it's not there.
|
||||
29 platforms ship compiled in: 5 you sign into, 24 you hand a key. Anything
|
||||
else is an endpoint you declare yourself ([your own endpoint](#your-own-endpoint)).
|
||||
|
||||
**Sign in with a subscription you already pay for.** Run `kigi login` and pick.
|
||||
Each provider's token is stored under its own key, and one provider's
|
||||
@@ -149,6 +149,28 @@ api_key = "sk-..."
|
||||
api_key = "xai-..."
|
||||
```
|
||||
|
||||
### Your own endpoint
|
||||
|
||||
Anything that speaks the OpenAI Chat Completions API or the Anthropic Messages
|
||||
API works: a gateway, a proxy, Ollama, LM Studio, vLLM. In `kigi`, open `/login`
|
||||
(or the welcome login screen) and pick **Custom provider (OpenAI compatible)**
|
||||
or **(Anthropic compatible)**, then enter a base URL, a name, and an API key.
|
||||
Kigi checks the key against
|
||||
`GET {base}/models`, saves the provider, and `/model` lists what the endpoint
|
||||
serves as `name/model`.
|
||||
|
||||
```toml
|
||||
# ~/.kigi/config.toml
|
||||
[platforms.my-gateway]
|
||||
base_url = "https://gateway.example.com/v1" # include /v1 when the API lives there
|
||||
api = "openai" # or "anthropic"
|
||||
api_key = "sk-..." # or store it in ~/.kigi/auth.json under "my-gateway"
|
||||
```
|
||||
|
||||
A key is required. A local server that ignores auth takes any value. The
|
||||
context window is 256K unless the endpoint reports one. To remove a provider,
|
||||
delete its table and its `auth.json` entry.
|
||||
|
||||
Model lists sync on startup. Pick one with `/model`, set its thinking level
|
||||
with `/effort`.
|
||||
|
||||
|
||||
@@ -127,3 +127,38 @@ async fn custom_anthropic_provider_uses_messages_and_x_api_key() {
|
||||
}
|
||||
harness.quit().expect("clean quit");
|
||||
}
|
||||
|
||||
/// A saved provider is a complete login: with no house key and no session, the
|
||||
/// pager starts authenticated and the turn reaches the provider.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn custom_provider_alone_authenticates_at_startup() {
|
||||
let content = ContentController::start_with_models(vec![MockModel::new("custom-m1")])
|
||||
.await
|
||||
.expect("start content");
|
||||
declare_provider(&content, "openai");
|
||||
content.set_response(format!(
|
||||
"{MOCK_RESPONSE_SENTINEL} from the custom provider."
|
||||
));
|
||||
let env: Vec<(String, String)> = content
|
||||
.env_for_pager()
|
||||
.into_iter()
|
||||
.filter(|(name, _)| name != "KIGI_API_KEY")
|
||||
.collect();
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new_in_dir(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs, None)
|
||||
.expect("spawn pager without a house key");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome text");
|
||||
harness
|
||||
.inject_keys(format!("{PROMPT}\r").as_bytes())
|
||||
.expect("submit prompt");
|
||||
harness
|
||||
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
|
||||
.expect("the turn reaches the custom provider without a login screen");
|
||||
harness.quit().expect("clean quit");
|
||||
}
|
||||
Reference in new issue
Block a user