jeo-code 0.9.17 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/config-schema.ts +20 -0
- package/src/agent/state.ts +19 -1
- package/src/ai/model-discovery.ts +9 -2
- package/src/ai/model-manager.ts +5 -2
- package/src/ai/provider-ranking.ts +153 -0
- package/src/ai/provider-registry.ts +10 -0
- package/src/ai/provider-status.ts +41 -2
- package/src/ai/providers/custom-providers.ts +321 -0
- package/src/ai/providers/openai-compatible-catalog.ts +31 -5
- package/src/ai/providers/provider-presets.ts +292 -0
- package/src/ai/register-providers.ts +54 -5
- package/src/ai/types.ts +10 -1
- package/src/cli/runner.ts +9 -0
- package/src/commands/launch/provider-slash.ts +413 -0
- package/src/commands/launch.ts +69 -48
- package/src/commands/provider.ts +199 -0
- package/src/tui/components/autocomplete.ts +28 -6
- package/src/tui/components/model-picker.ts +18 -8
package/src/commands/launch.ts
CHANGED
|
@@ -67,6 +67,16 @@ import { isRateLimitError, isUsageLimitError, isConnectionError } from "../util/
|
|
|
67
67
|
|
|
68
68
|
import { listAliases } from "../ai/model-registry";
|
|
69
69
|
import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
|
|
70
|
+
import {
|
|
71
|
+
applyCustomProviderPatch,
|
|
72
|
+
formatCustomProviderList,
|
|
73
|
+
formatPresetsCommand,
|
|
74
|
+
looksLikePreset,
|
|
75
|
+
parseProviderAddArgs,
|
|
76
|
+
planProviderAdd,
|
|
77
|
+
planProviderRemove,
|
|
78
|
+
providerAddUsage,
|
|
79
|
+
} from "./launch/provider-slash";
|
|
70
80
|
|
|
71
81
|
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting, applyTargetChoices } from "../agent/subagents";
|
|
72
82
|
import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
|
|
@@ -4640,8 +4650,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4640
4650
|
" OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity|<subscription>] (alias: /login)",
|
|
4641
4651
|
" subscriptions (token): alibaba-coding-plan, qwen-portal, xiaomi-token-plan-*, minimax-code*",
|
|
4642
4652
|
" API key (cloud) : /provider key [provider] [key] (groq, deepseek, mistral, openrouter, …)",
|
|
4643
|
-
" API-compatible : /provider add --base-url <url> [--
|
|
4644
|
-
"
|
|
4653
|
+
" API-compatible : /provider add --id <id> --base-url <url> [--compat openai|anthropic] [--model a,b]",
|
|
4654
|
+
" presets: /provider add --preset <id> [--base-url <url>] · list: /provider presets",
|
|
4655
|
+
" manage: /provider list · /provider remove <id>",
|
|
4656
|
+
" legacy (rebinds openai/): /provider add --base-url <url> · clear: /provider add clear",
|
|
4645
4657
|
" Logout : /logout <provider>",
|
|
4646
4658
|
" Headless OAuth : paste the redirect URL or code when the login prompt asks.",
|
|
4647
4659
|
"Switch the active model or provider with /model.",
|
|
@@ -4656,8 +4668,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4656
4668
|
else if (action === "api-key") name = "key";
|
|
4657
4669
|
else if (action === "api-add") {
|
|
4658
4670
|
console.log("Add an API-compatible endpoint:");
|
|
4659
|
-
|
|
4660
|
-
console.log(" reads OPENAI_API_KEY · show current / clear: /provider add · /provider add clear");
|
|
4671
|
+
for (const line of providerAddUsage()) console.log(line);
|
|
4661
4672
|
continue;
|
|
4662
4673
|
}
|
|
4663
4674
|
// action === undefined (cancelled) → fall through to the readiness panel.
|
|
@@ -4810,56 +4821,63 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4810
4821
|
}
|
|
4811
4822
|
continue;
|
|
4812
4823
|
}
|
|
4813
|
-
// `/provider
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4824
|
+
// `/provider presets` → the preset catalog (fixed + parameterized gateways).
|
|
4825
|
+
if (name === "presets" || name === "preset") {
|
|
4826
|
+
logLines(formatPresetsCommand());
|
|
4827
|
+
continue;
|
|
4828
|
+
}
|
|
4829
|
+
// `/provider list` → registered custom providers (built-ins stay in the panel).
|
|
4830
|
+
if (name === "list" || name === "ls") {
|
|
4831
|
+
logLines(formatCustomProviderList(await readGlobalConfig()));
|
|
4832
|
+
continue;
|
|
4833
|
+
}
|
|
4834
|
+
// `/provider remove <id>` → unregister a custom provider.
|
|
4835
|
+
if (name === "remove" || name === "rm" || name === "delete") {
|
|
4836
|
+
const decision = planProviderRemove(tokens[1], await readGlobalConfig());
|
|
4837
|
+
logLines(decision.lines);
|
|
4838
|
+
if (decision.removeId) {
|
|
4839
|
+
await saveConfigPatch(raw => ({
|
|
4840
|
+
customProviders: applyCustomProviderPatch(raw.customProviders, { removeId: decision.removeId }),
|
|
4841
|
+
}));
|
|
4842
|
+
// Re-read so `withEnvOverlay` republishes the surviving set into the runtime
|
|
4843
|
+
// registry (the listener in register-providers unregisters the dropped adapter).
|
|
4844
|
+
await readGlobalConfig();
|
|
4820
4845
|
await refreshLiveModelsCache();
|
|
4821
|
-
console.log("OpenAI-compatible base URL cleared — saved to ~/.jeo/config.json.");
|
|
4822
|
-
continue;
|
|
4823
4846
|
}
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
if (
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (!baseUrl) {
|
|
4841
|
-
const cur = (await readGlobalConfig()).openaiBaseUrl;
|
|
4842
|
-
console.log(cur ? `OpenAI-compatible base URL: ${cur}` : "No OpenAI-compatible base URL set.");
|
|
4843
|
-
console.log("Set one with: /provider add --base-url <url> [--model <model>] · clear with: /provider add clear");
|
|
4844
|
-
continue;
|
|
4847
|
+
continue;
|
|
4848
|
+
}
|
|
4849
|
+
// `/provider add …` → register a NAMED custom provider (--id / --preset), or, in the
|
|
4850
|
+
// legacy no-id spelling, rebind the built-in openai endpoint. All parsing/validation
|
|
4851
|
+
// lives in provider-slash.ts so it is unit-testable without a TUI.
|
|
4852
|
+
if (name === "add") {
|
|
4853
|
+
const parsedAdd = parseProviderAddArgs(tokens.slice(1));
|
|
4854
|
+
// `/provider add litellm --base-url …` — a bare preset name is accepted as --preset.
|
|
4855
|
+
if (!parsedAdd.preset && looksLikePreset(tokens[1])) parsedAdd.preset = tokens[1];
|
|
4856
|
+
const cfgNow = await readGlobalConfig();
|
|
4857
|
+
const plan = planProviderAdd(parsedAdd, cfgNow);
|
|
4858
|
+
logLines(plan.lines);
|
|
4859
|
+
if (plan.noop) continue;
|
|
4860
|
+
if (plan.legacyOpenaiBaseUrl !== undefined) {
|
|
4861
|
+
const url = plan.legacyOpenaiBaseUrl;
|
|
4862
|
+
await saveConfigPatch(() => ({ openaiBaseUrl: url ?? undefined }));
|
|
4845
4863
|
}
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4864
|
+
if (plan.id && plan.config) {
|
|
4865
|
+
await saveConfigPatch(raw => ({
|
|
4866
|
+
customProviders: applyCustomProviderPatch(raw.customProviders, { addId: plan.id, addConfig: plan.config }),
|
|
4867
|
+
}));
|
|
4868
|
+
// Republish into the runtime registry before discovery runs, so the very next
|
|
4869
|
+
// /models probe already targets the new endpoint.
|
|
4870
|
+
await readGlobalConfig();
|
|
4850
4871
|
}
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
sessionModel = qualified;
|
|
4855
|
-
await saveConfigPatch(raw => rememberModelPatch(raw, qualified));
|
|
4872
|
+
if (plan.selectModel) {
|
|
4873
|
+
sessionModel = plan.selectModel;
|
|
4874
|
+
await saveConfigPatch(raw => rememberModelPatch(raw, plan.selectModel!));
|
|
4856
4875
|
await persistSessionModel();
|
|
4857
|
-
console.log(`
|
|
4858
|
-
} else {
|
|
4859
|
-
console.log(`OpenAI-compatible endpoint set: ${url} — saved to ~/.jeo/config.json.`);
|
|
4876
|
+
console.log(`Active model → ${plan.selectModel}`);
|
|
4860
4877
|
}
|
|
4878
|
+
const probeProvider = plan.id ?? "openai";
|
|
4861
4879
|
const live = await refreshLiveModelsCache();
|
|
4862
|
-
const forProvider = live.filter(r => r.provider ===
|
|
4880
|
+
const forProvider = live.filter(r => r.provider === probeProvider);
|
|
4863
4881
|
const pick = flattenModels(forProvider);
|
|
4864
4882
|
if (pick.length) {
|
|
4865
4883
|
lastPickIndex = pick;
|
|
@@ -4867,7 +4885,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4867
4885
|
logLines(formatPickListWithCapabilities(pick, { cap: 12 }));
|
|
4868
4886
|
} else {
|
|
4869
4887
|
const failed = forProvider.find(r => !r.ok);
|
|
4870
|
-
|
|
4888
|
+
const envHint = plan.id
|
|
4889
|
+
? `set ${plan.config?.apiKeyEnv ?? `${plan.id.replace(/[.\-]/g, "_").toUpperCase()}_API_KEY`}`
|
|
4890
|
+
: "set OPENAI_API_KEY";
|
|
4891
|
+
console.log(failed?.error ? ` could not list models: ${failed.error}` : ` no models discovered yet — the endpoint may need a key (${envHint}).`);
|
|
4871
4892
|
}
|
|
4872
4893
|
continue;
|
|
4873
4894
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jeo provider …` — manage providers from the shell, not just from inside the REPL.
|
|
3
|
+
*
|
|
4
|
+
* The slash command (`/provider add …`) already existed, but it is only reachable from
|
|
5
|
+
* an interactive session. Provisioning a machine, a CI runner, or a devcontainer means
|
|
6
|
+
* scripting it, so the same registry is exposed as a real subcommand with the SAME
|
|
7
|
+
* flags and the SAME planners (`provider-slash.ts`) — one implementation, two surfaces,
|
|
8
|
+
* so they can never drift.
|
|
9
|
+
*
|
|
10
|
+
* Beyond parity this adds `jeo provider test <id>`, a live endpoint probe. Registering a
|
|
11
|
+
* provider is cheap and silent; discovering at the first real inference call that the
|
|
12
|
+
* base URL was wrong (or the key env var is not exported in THIS shell) is not. `add`
|
|
13
|
+
* runs the probe automatically and reports the result without refusing the write — a
|
|
14
|
+
* user may legitimately register an endpoint before the key exists.
|
|
15
|
+
*/
|
|
16
|
+
import { readGlobalConfig, saveConfigPatch } from "../agent/state";
|
|
17
|
+
import {
|
|
18
|
+
applyCustomProviderPatch,
|
|
19
|
+
formatCustomProviderList,
|
|
20
|
+
formatPresetsCommand,
|
|
21
|
+
looksLikePreset,
|
|
22
|
+
parseProviderAddArgs,
|
|
23
|
+
planProviderAdd,
|
|
24
|
+
planProviderRemove,
|
|
25
|
+
providerAddUsage,
|
|
26
|
+
} from "./launch/provider-slash";
|
|
27
|
+
import {
|
|
28
|
+
credentialSourceOf,
|
|
29
|
+
customProviderDef,
|
|
30
|
+
customProviderDefs,
|
|
31
|
+
normalizeProviderId,
|
|
32
|
+
} from "../ai/providers/custom-providers";
|
|
33
|
+
import { listProviderModels } from "../ai/model-discovery";
|
|
34
|
+
import { describeProvider } from "../ai/provider-status";
|
|
35
|
+
|
|
36
|
+
const USAGE = [
|
|
37
|
+
"Usage: jeo provider <command>",
|
|
38
|
+
"",
|
|
39
|
+
" list Show registered custom providers",
|
|
40
|
+
" add --id <id> --base-url <url> [...] Register an OpenAI/Anthropic-compatible provider",
|
|
41
|
+
" add --preset <preset> [--base-url <url>]",
|
|
42
|
+
" remove <id> Unregister a custom provider",
|
|
43
|
+
" presets List the built-in provider presets",
|
|
44
|
+
" test <id> Probe a provider's endpoint and credential",
|
|
45
|
+
"",
|
|
46
|
+
"Flags for add:",
|
|
47
|
+
" --id <id> Routing prefix + config key (e.g. my-proxy → my-proxy/<model>)",
|
|
48
|
+
" --base-url <url> API base URL (https://… or http://…)",
|
|
49
|
+
" --compat <protocol> openai (default) | anthropic",
|
|
50
|
+
" --api-key-env <ENV> Env var holding the key (default: <ID>_API_KEY)",
|
|
51
|
+
" --api-key <key> Literal key, stored in ~/.jeo/config.json (prefer --api-key-env)",
|
|
52
|
+
" --model a,b Known model ids for the offline pick-list",
|
|
53
|
+
" --label <name> Display name",
|
|
54
|
+
" --force Overwrite an existing provider with the same id",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
export async function runProviderCommand(args: string[]): Promise<void> {
|
|
58
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
59
|
+
switch (sub) {
|
|
60
|
+
case "":
|
|
61
|
+
case "help":
|
|
62
|
+
case "--help":
|
|
63
|
+
case "-h":
|
|
64
|
+
console.log(USAGE);
|
|
65
|
+
return;
|
|
66
|
+
case "list":
|
|
67
|
+
case "ls":
|
|
68
|
+
return runList();
|
|
69
|
+
case "presets":
|
|
70
|
+
case "preset":
|
|
71
|
+
console.log(formatPresetsCommand().join("\n"));
|
|
72
|
+
return;
|
|
73
|
+
case "add":
|
|
74
|
+
return runAdd(args.slice(1));
|
|
75
|
+
case "remove":
|
|
76
|
+
case "rm":
|
|
77
|
+
case "delete":
|
|
78
|
+
return runRemove(args[1]);
|
|
79
|
+
case "test":
|
|
80
|
+
case "probe":
|
|
81
|
+
case "check":
|
|
82
|
+
return runTest(args[1]);
|
|
83
|
+
default:
|
|
84
|
+
console.log(`Unknown provider subcommand: ${sub}\n\n${USAGE}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runList(): Promise<void> {
|
|
90
|
+
console.log(formatCustomProviderList(await readGlobalConfig()).join("\n"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runAdd(rest: string[]): Promise<void> {
|
|
94
|
+
const parsed = parseProviderAddArgs(rest);
|
|
95
|
+
if (!parsed.preset && looksLikePreset(rest[0])) parsed.preset = rest[0];
|
|
96
|
+
|
|
97
|
+
const plan = planProviderAdd(parsed, await readGlobalConfig());
|
|
98
|
+
console.log(plan.lines.join("\n"));
|
|
99
|
+
if (plan.noop) {
|
|
100
|
+
// A usage dump is informational; a rejected registration is a scripting failure and
|
|
101
|
+
// must be visible to `set -e`. `--base-url`-less bare invocations print usage only.
|
|
102
|
+
if (rest.length > 0) process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (plan.legacyOpenaiBaseUrl !== undefined) {
|
|
107
|
+
const url = plan.legacyOpenaiBaseUrl;
|
|
108
|
+
await saveConfigPatch(() => ({ openaiBaseUrl: url ?? undefined }));
|
|
109
|
+
}
|
|
110
|
+
if (plan.id && plan.config) {
|
|
111
|
+
await saveConfigPatch(raw => ({
|
|
112
|
+
customProviders: applyCustomProviderPatch(raw.customProviders, { addId: plan.id, addConfig: plan.config }),
|
|
113
|
+
}));
|
|
114
|
+
// Re-read so `withEnvOverlay` republishes the new provider into the runtime registry
|
|
115
|
+
// before the probe below resolves its credential.
|
|
116
|
+
await readGlobalConfig();
|
|
117
|
+
console.log("");
|
|
118
|
+
await probe(plan.id, { failOnError: false });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runRemove(rawId: string | undefined): Promise<void> {
|
|
123
|
+
const decision = planProviderRemove(rawId, await readGlobalConfig());
|
|
124
|
+
console.log(decision.lines.join("\n"));
|
|
125
|
+
if (!decision.removeId) {
|
|
126
|
+
if (rawId) process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await saveConfigPatch(raw => ({
|
|
130
|
+
customProviders: applyCustomProviderPatch(raw.customProviders, { removeId: decision.removeId }),
|
|
131
|
+
}));
|
|
132
|
+
await readGlobalConfig();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runTest(rawId: string | undefined): Promise<void> {
|
|
136
|
+
if (!rawId) {
|
|
137
|
+
const ids = customProviderDefs().map(d => d.name);
|
|
138
|
+
console.log(
|
|
139
|
+
ids.length
|
|
140
|
+
? `Usage: jeo provider test <id>\nRegistered: ${ids.join(", ")}`
|
|
141
|
+
: "No custom providers registered. Add one with 'jeo provider add --id <id> --base-url <url>'.",
|
|
142
|
+
);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await probe(normalizeProviderId(rawId), { failOnError: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Probe one provider: report its credential source, then try to list models at its
|
|
151
|
+
* endpoint. Separating the two matters — "no key" and "wrong URL" need different fixes,
|
|
152
|
+
* and a single "failed" line sends users down the wrong path.
|
|
153
|
+
*/
|
|
154
|
+
async function probe(id: string, opts: { failOnError: boolean }): Promise<void> {
|
|
155
|
+
// A fresh config read republishes the custom set, so a provider added moments ago in
|
|
156
|
+
// this same process is resolvable here.
|
|
157
|
+
const cfg = await readGlobalConfig();
|
|
158
|
+
const def = customProviderDef(id);
|
|
159
|
+
if (!def) {
|
|
160
|
+
const status = await describeProvider(id, cfg);
|
|
161
|
+
if (!status.ready && status.kind === "none") {
|
|
162
|
+
console.log(`No provider '${id}'. Run 'jeo provider list' or 'jeo provider add --id ${id} --base-url <url>'.`);
|
|
163
|
+
if (opts.failOnError) process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`'${id}' is a built-in provider — check it with 'jeo doctor'.`);
|
|
167
|
+
if (opts.failOnError) process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const source = credentialSourceOf(def);
|
|
172
|
+
console.log(`Probing ${id} → ${def.baseUrl} (${def.protocol ?? "openai"})`);
|
|
173
|
+
if (source === "none") {
|
|
174
|
+
console.log(` credential: MISSING — export ${def.apiKeyEnv}, or re-add with --api-key <key>.`);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` credential: ${source === "env" ? `env ${def.apiKeyEnv}` : "stored in config"}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const result = await listProviderModels(id, { timeoutMs: 8000 });
|
|
180
|
+
if (result.ok) {
|
|
181
|
+
// Discovery already returns ids the router accepts; `qualify` only guards the case
|
|
182
|
+
// where an endpoint echoes bare ids so the printed command is copy-pasteable.
|
|
183
|
+
const qualify = (m: string) => (m.startsWith(`${id}/`) ? m : `${id}/${m}`);
|
|
184
|
+
const shown = result.models.slice(0, 8).map(qualify);
|
|
185
|
+
console.log(` models : ${result.models.length} discovered${result.fallback ? " (from the static catalog — the live list endpoint was unusable)" : ""}`);
|
|
186
|
+
if (shown.length) console.log(` ${shown.join(", ")}${result.models.length > shown.length ? ", …" : ""}`);
|
|
187
|
+
const first = result.models[0];
|
|
188
|
+
console.log(` status : OK — use it with 'jeo launch --model ${first ? qualify(first) : `${id}/<model>`}'`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(` status : FAILED — ${result.error ?? "no models returned"}`);
|
|
193
|
+
console.log(
|
|
194
|
+
source === "none"
|
|
195
|
+
? ` next : set ${def.apiKeyEnv} and re-run 'jeo provider test ${id}'.`
|
|
196
|
+
: ` next : verify the base URL (a ${def.protocol === "anthropic" ? "/v1/models" : "/models"} route must exist under it) and that the key is valid for this endpoint.`,
|
|
197
|
+
);
|
|
198
|
+
if (opts.failOnError) process.exitCode = 1;
|
|
199
|
+
}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { SLASH_COMMANDS, SLASH_COMMAND_DESCRIPTIONS } from "./slash";
|
|
17
17
|
import { catalogIds } from "../../ai/model-catalog-compat";
|
|
18
|
-
import { PROVIDER_NAMES } from "../../ai/provider-status";
|
|
18
|
+
import { PROVIDER_NAMES, allProviderNames } from "../../ai/provider-status";
|
|
19
|
+
import { PROVIDER_PRESET_IDS } from "../../ai/providers/provider-presets";
|
|
20
|
+
import { customProviderNames } from "../../ai/providers/custom-providers";
|
|
19
21
|
import { SUBAGENT_ROLES } from "../../agent/subagents";
|
|
20
22
|
import { skillNames } from "../../skills/catalog";
|
|
21
23
|
import { listThemes } from "./themes";
|
|
@@ -35,6 +37,10 @@ export interface CompletionContext {
|
|
|
35
37
|
skillNames?: string[];
|
|
36
38
|
/** Live model ids for a given provider (for `/provider <p> <model>`). */
|
|
37
39
|
modelsForProvider: (provider: string) => string[];
|
|
40
|
+
/** Preset ids offered after `/provider add --preset`. */
|
|
41
|
+
providerPresets: string[];
|
|
42
|
+
/** Ids of the user's registered custom providers (for `/provider remove <id>`). */
|
|
43
|
+
customProviders: string[];
|
|
38
44
|
/** Sync path suggestions for free-text `@path` mentions (relative to cwd). */
|
|
39
45
|
mentionPaths?: (prefix: string) => string[];
|
|
40
46
|
}
|
|
@@ -66,9 +72,13 @@ export function staticCompletionContext(): Omit<CompletionContext, "liveModels"
|
|
|
66
72
|
return {
|
|
67
73
|
slashCommands: [...SLASH_COMMANDS],
|
|
68
74
|
catalogModels: catalogIds(),
|
|
69
|
-
providers
|
|
75
|
+
// Custom providers are runtime state, so list from `allProviderNames()` — a user's
|
|
76
|
+
// own endpoint must be completable the same as a shipped one.
|
|
77
|
+
providers: [...allProviderNames()],
|
|
70
78
|
roleIds: SUBAGENT_ROLES.map(r => r.id),
|
|
71
79
|
thinkingLevels: [...THINKING_LEVELS],
|
|
80
|
+
providerPresets: [...PROVIDER_PRESET_IDS],
|
|
81
|
+
customProviders: [...customProviderNames()],
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -271,13 +281,25 @@ export function complete(line: string, ctx: CompletionContext): CompletionResult
|
|
|
271
281
|
case "/fast":
|
|
272
282
|
return argIndex === 0 ? finish(["on", "off", "status"], "subcommand") : { completions: [], token, kind: "none" };
|
|
273
283
|
case "/provider": {
|
|
274
|
-
// /provider is onboarding-only (gjc parity): login
|
|
275
|
-
// switching completes under /model
|
|
284
|
+
// /provider is onboarding-only (gjc parity): login, key, and the custom-provider
|
|
285
|
+
// registry (add/list/remove/presets). Model switching completes under /model.
|
|
276
286
|
const cloud = ["anthropic", "openai", "gemini", "antigravity"];
|
|
277
|
-
if (argIndex === 0) return finish(["login", "add", "help"], "subcommand");
|
|
287
|
+
if (argIndex === 0) return finish(["login", "key", "add", "list", "remove", "presets", "help"], "subcommand");
|
|
278
288
|
const sub = tokens[1]?.toLowerCase();
|
|
279
289
|
if (sub === "login" || sub === "auth") return argIndex === 1 ? finish(cloud, "provider") : { completions: [], token, kind: "none" };
|
|
280
|
-
|
|
290
|
+
// Removal only ever targets a CUSTOM provider, so completing built-ins here would
|
|
291
|
+
// just offer choices the command refuses.
|
|
292
|
+
if (sub === "remove" || sub === "rm" || sub === "delete") {
|
|
293
|
+
return argIndex === 1 ? finish([...ctx.customProviders], "provider") : { completions: [], token, kind: "none" };
|
|
294
|
+
}
|
|
295
|
+
if (sub === "add") {
|
|
296
|
+
// Right after `--preset`, offer preset ids; otherwise the flag set.
|
|
297
|
+
if (tokens[argIndex]?.toLowerCase() === "--preset") return finish([...ctx.providerPresets], "subcommand");
|
|
298
|
+
return finish(
|
|
299
|
+
["--id", "--base-url", "--compat", "--api-key-env", "--api-key", "--model", "--label", "--preset", "--force", "clear"],
|
|
300
|
+
"subcommand",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
281
303
|
return { completions: [], token, kind: "none" };
|
|
282
304
|
}
|
|
283
305
|
case "/logout":
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { SelectList, renderSelectList, type SelectItem, type RenderSelectOptions } from "./select-list";
|
|
8
8
|
import { catalogForProvider, type ModelCatalogEntry } from "../../ai/model-catalog-compat";
|
|
9
9
|
import { companyLabel } from "../../ai/model-catalog";
|
|
10
|
-
import { PROVIDER_NAMES, type ProviderStatus } from "../../ai/provider-status";
|
|
10
|
+
import { PROVIDER_NAMES, allProviderNames, type ProviderStatus } from "../../ai/provider-status";
|
|
11
|
+
import { authStateFor, sortRankedProviders } from "../../ai/provider-ranking";
|
|
11
12
|
import type { ProviderName } from "../../ai/types";
|
|
12
13
|
|
|
13
14
|
/** Human context-window size: 200000 → "200k", 1000000 → "1M", 0 → "". */
|
|
@@ -46,14 +47,23 @@ export function buildModelChoices(statuses: ProviderStatus[], opts: ModelPickerO
|
|
|
46
47
|
const unicode = opts.unicode !== false;
|
|
47
48
|
const includeUnready = opts.includeUnready !== false;
|
|
48
49
|
const readyOf = new Map<ProviderName, boolean>(statuses.map(s => [s.name, s.ready]));
|
|
50
|
+
const statusOf = new Map<ProviderName, ProviderStatus>(statuses.map(s => [s.name, s]));
|
|
49
51
|
|
|
50
|
-
// Provider order
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
// Provider order comes from the SHARED ranking (usable → broken → famous → rest), not
|
|
53
|
+
// this component's own list-index sort. That old sort ranked by `PROVIDER_NAMES.indexOf`,
|
|
54
|
+
// which returns -1 for a user-registered custom provider — floating it above the
|
|
55
|
+
// provider the user is actually logged into.
|
|
56
|
+
const providers = sortRankedProviders(
|
|
57
|
+
[...allProviderNames()].map(name => {
|
|
58
|
+
const status = statusOf.get(name);
|
|
59
|
+
return {
|
|
60
|
+
id: name,
|
|
61
|
+
label: companyLabel(name),
|
|
62
|
+
authState: status ? authStateFor(status) : ("none" as const),
|
|
63
|
+
custom: status?.custom,
|
|
64
|
+
};
|
|
65
|
+
}),
|
|
66
|
+
).map(p => p.id as ProviderName);
|
|
57
67
|
|
|
58
68
|
const items: SelectItem<string>[] = [];
|
|
59
69
|
for (const provider of providers) {
|