@vincemakes/kiso-code 0.30.0 → 0.31.1
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/dist/auth/credentials.d.ts +39 -0
- package/dist/auth/credentials.js +170 -0
- package/dist/auth/oauth/chatgpt.d.ts +40 -0
- package/dist/auth/oauth/chatgpt.js +203 -0
- package/dist/auth/oauth/index.d.ts +37 -0
- package/dist/auth/oauth/index.js +1 -0
- package/dist/auth/oauth/pkce.d.ts +5 -0
- package/dist/auth/oauth/pkce.js +12 -0
- package/dist/auth/refresh.d.ts +12 -0
- package/dist/auth/refresh.js +40 -0
- package/dist/auth/token.d.ts +17 -0
- package/dist/auth/token.js +26 -0
- package/dist/chat.d.ts +10 -1
- package/dist/chat.js +50 -8
- package/dist/config.d.ts +42 -6
- package/dist/config.js +87 -12
- package/dist/dispatch.js +117 -11
- package/dist/index.js +238 -30
- package/dist/mode.d.ts +2 -2
- package/dist/mode.js +2 -2
- package/dist/state.d.ts +7 -2
- package/dist/state.js +5 -1
- package/dist/trust-ui.d.ts +13 -0
- package/dist/trust-ui.js +17 -2
- package/dist/update-check.d.ts +9 -3
- package/dist/update-check.js +27 -28
- package/package.json +15 -14
- package/dist/diff.d.ts +0 -32
- package/dist/diff.js +0 -122
package/dist/chat.js
CHANGED
|
@@ -11,7 +11,7 @@ import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-n
|
|
|
11
11
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
12
12
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
13
13
|
import { dispatch } from "./dispatch.js";
|
|
14
|
-
import { agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
14
|
+
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
15
15
|
import { attachImages } from "./attachments.js";
|
|
16
16
|
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
17
17
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
@@ -47,8 +47,10 @@ export function contextWindowTokens() {
|
|
|
47
47
|
// window (and the microcompact threshold derived from it) without an
|
|
48
48
|
// env var. agentModel is the same live binding the status row shows;
|
|
49
49
|
// an unknown model keeps the 200k default — the registry never
|
|
50
|
-
// guesses, so neither do we.
|
|
51
|
-
|
|
50
|
+
// guesses, so neither do we. OR-1: the ENDPOINT narrows the row —
|
|
51
|
+
// gpt-5.5 is 1,050,000 at the first-party API and 272,000 at the
|
|
52
|
+
// subscription backend; the two are set together (setAgentModel).
|
|
53
|
+
const known = lookupModelMetadata(agentModel, agentBaseUrl)?.capabilities.contextWindow;
|
|
52
54
|
if (known !== undefined && known !== null)
|
|
53
55
|
return known;
|
|
54
56
|
return DEFAULT_CONTEXT_WINDOW;
|
|
@@ -67,6 +69,14 @@ export function estimateCtxRatio(session) {
|
|
|
67
69
|
* their continuation envelopes, the output reserve when known) over the
|
|
68
70
|
* window. Still ~ (chars/4). The auto-compact policy does NOT read this
|
|
69
71
|
* (see autoCompactRatio) — moving the policy's number is A1b's. */
|
|
72
|
+
/** OR-8 (owner, 2026-09-09): the status row names the effort next to the
|
|
73
|
+
* model — `gpt-5.6-sol · xhigh` — whenever the live binding carries one;
|
|
74
|
+
* the provider's default shows as the bare model, as before. The effort
|
|
75
|
+
* is the session's own (the durable profile), never the CLI's memory. */
|
|
76
|
+
export function statusModelLabel(session) {
|
|
77
|
+
const effort = session.reasoning?.effort;
|
|
78
|
+
return effort !== undefined && effort !== "default" ? `${agentModel} · ${effort}` : agentModel;
|
|
79
|
+
}
|
|
70
80
|
export function displayCtxRatio(session) {
|
|
71
81
|
return requestBudget(session.requestParts(), contextWindowTokens()).ratio;
|
|
72
82
|
}
|
|
@@ -110,8 +120,12 @@ export function usageFromEvent(route, ev, prevTotal,
|
|
|
110
120
|
// metadata entry (an unpriced model shows null, never a route-table
|
|
111
121
|
// guess); omitted, the legacy route-keyed path stands (old callers,
|
|
112
122
|
// old tests, unchanged bytes).
|
|
113
|
-
model
|
|
114
|
-
|
|
123
|
+
model,
|
|
124
|
+
// OR-1: the live ENDPOINT — the same model id is priced at the
|
|
125
|
+
// first-party API and unpriced at the subscription backend; the
|
|
126
|
+
// session hands it over next to `provider` (one binding, one row).
|
|
127
|
+
endpoint) {
|
|
128
|
+
const c = model === undefined ? canonicalizeUsage(route ?? "adapter", ev) : canonicalizeUsageForModel(model, endpoint, route ?? "adapter", ev);
|
|
115
129
|
const total = c.input + c.cacheRead + (c.cacheWrite ?? 0);
|
|
116
130
|
let missed = null;
|
|
117
131
|
// R-C item 4: min(prevTotal, total) is the part that could have been
|
|
@@ -121,7 +135,24 @@ model) {
|
|
|
121
135
|
const m = Math.min(prevTotal, total) - c.cacheRead;
|
|
122
136
|
missed = m > CACHE_MISS_FLOOR ? m : null;
|
|
123
137
|
}
|
|
124
|
-
|
|
138
|
+
// OR-10 (owner, 2026-09-09): a backend whose cache figure cannot be
|
|
139
|
+
// observed has no cache figure. The ChatGPT backend answers
|
|
140
|
+
// `cached_tokens` 0 to a third-party client on every request, so its 0
|
|
141
|
+
// and "not reported" are one value, and a CH 0% built on it claims a
|
|
142
|
+
// measurement that never happened. The registry ROW says which
|
|
143
|
+
// endpoints these are; the status row, the recap and the usage line
|
|
144
|
+
// already render a null cache as nothing ("an unmeasured cache is not
|
|
145
|
+
// a 0% cache"), so this is one decision at the one place the endpoint
|
|
146
|
+
// is known. The trace ledger is written by the runtime from the
|
|
147
|
+
// backend's own words and keeps its 0 — the display's rule, not the
|
|
148
|
+
// record's.
|
|
149
|
+
const unobservable = model !== undefined && lookupModelMetadata(model, endpoint)?.capabilities.promptCaching === "unobservable";
|
|
150
|
+
return {
|
|
151
|
+
usage: { in: c.input, out: c.output, cache: unobservable ? null : c.cacheRead, known: ev.known },
|
|
152
|
+
total,
|
|
153
|
+
missed: unobservable ? null : missed,
|
|
154
|
+
costUsd: c.costUsd,
|
|
155
|
+
};
|
|
125
156
|
}
|
|
126
157
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
127
158
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
@@ -636,7 +667,7 @@ submitTurn) {
|
|
|
636
667
|
body.notice("stream interrupted — the draft above is abandoned");
|
|
637
668
|
break;
|
|
638
669
|
case "usage": {
|
|
639
|
-
const delta = usageFromEvent(session.provider, ev, prevTotal, agentModel);
|
|
670
|
+
const delta = usageFromEvent(session.provider, ev, prevTotal, agentModel, session.baseUrl);
|
|
640
671
|
usage = delta.usage;
|
|
641
672
|
prevTotal = delta.total;
|
|
642
673
|
missed = delta.missed;
|
|
@@ -785,6 +816,17 @@ submitTurn) {
|
|
|
785
816
|
// is a sentence addressed to a human; it needs no edge.
|
|
786
817
|
body.notice('answer truncated at max_tokens — say "continue" to finish');
|
|
787
818
|
}
|
|
819
|
+
// OR-5 (the ChatGPT real leg, 2026-09-09): a run that ended in an
|
|
820
|
+
// ERROR terminal printed nothing but the recap — the vendor's 400
|
|
821
|
+
// lived in the durable log alone and the turn read as "took 1s".
|
|
822
|
+
// A failure that fires silently is indistinguishable from a
|
|
823
|
+
// crash, the one thing this product must never be: the mapped
|
|
824
|
+
// error is named here, before the recap, code and status and the
|
|
825
|
+
// provider's own words.
|
|
826
|
+
if (ev.outcome.kind === "error") {
|
|
827
|
+
const e = ev.outcome.error;
|
|
828
|
+
body.notice(`run failed — ${e.code}${e.status !== undefined ? ` ${e.status}` : ""}${e.retryable ? " (retryable)" : ""}: ${escapeTerminal(e.message)}`);
|
|
829
|
+
}
|
|
788
830
|
bodyLog(renderRecap({
|
|
789
831
|
seconds: Math.round((Date.now() - turnStart) / 1000),
|
|
790
832
|
// R3g: the work terms are NOT passed — the fold line
|
|
@@ -1077,7 +1119,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1077
1119
|
// TUI2-R1 (E): the meter rides the idle row — both fields omitted
|
|
1078
1120
|
// when unknown, so a session that has not called the model paints
|
|
1079
1121
|
// exactly the pre-round row.
|
|
1080
|
-
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(),
|
|
1122
|
+
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session), {
|
|
1081
1123
|
cacheHitPct: cacheHitPct(runUsage),
|
|
1082
1124
|
costUsd: spentUsd,
|
|
1083
1125
|
}));
|
package/dist/config.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Schema v1:
|
|
10
10
|
* model?: string — a profile NAME (models.<name>) or "provider/model"
|
|
11
|
-
* direct write (provider: "openai-compat" |
|
|
12
|
-
*
|
|
11
|
+
* direct write (provider: "openai-compat" |
|
|
12
|
+
* "anthropic" | "openai-responses")
|
|
13
|
+
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
13
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
14
15
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
16
|
* contextWindow?: number — tokens
|
|
@@ -27,7 +28,11 @@
|
|
|
27
28
|
* ignored (forward compatibility).
|
|
28
29
|
*/
|
|
29
30
|
import type { Mode } from "./mode.js";
|
|
30
|
-
|
|
31
|
+
/** OR-1: `openai-responses` is a DIALECT, not a vendor — the same kind
|
|
32
|
+
* drives the first-party Responses API and the ChatGPT subscription
|
|
33
|
+
* backend, and which one it is follows the profile's baseUrl (and so
|
|
34
|
+
* the credential it resolves to). */
|
|
35
|
+
export type ProfileKind = "openai-compat" | "anthropic" | "openai-responses";
|
|
31
36
|
export interface ModelProfile {
|
|
32
37
|
readonly kind: ProfileKind;
|
|
33
38
|
readonly baseUrl?: string;
|
|
@@ -82,13 +87,44 @@ export declare function loadUserConfig(): KisoConfig | null;
|
|
|
82
87
|
export declare function loadProjectConfig(cwd: string, trusted: boolean): KisoConfig | null;
|
|
83
88
|
/** Merge: project wins over user (each layer's own keys only). */
|
|
84
89
|
export declare function mergeConfigs(user: KisoConfig | null, project: KisoConfig | null): KisoConfig;
|
|
85
|
-
/**
|
|
90
|
+
/** What a profile's sign-in actually IS. The two shapes are not
|
|
91
|
+
* interchangeable: an API key is a string an adapter holds, an OAuth
|
|
92
|
+
* sign-in is a token that expires and must be re-resolved per request. */
|
|
93
|
+
export type ProfileAuth = {
|
|
94
|
+
readonly type: "api-key";
|
|
95
|
+
readonly apiKey: string;
|
|
96
|
+
readonly source: "store" | "env" | "none";
|
|
97
|
+
} | {
|
|
98
|
+
readonly type: "oauth";
|
|
99
|
+
readonly providerId: string;
|
|
100
|
+
};
|
|
101
|
+
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
102
|
+
* plan's resolve rule: a stored credential OWNS the provider (an
|
|
103
|
+
* unusable stored one is an error, never a silent fall back to env);
|
|
104
|
+
* with nothing stored, the profile's env var applies; a keyless profile
|
|
105
|
+
* (no apiKeyEnv — PH-1c, finding PH-F19) is an unauthenticated endpoint.
|
|
106
|
+
*
|
|
107
|
+
* OR-1: a stored OAuth credential is USABLE only by an adapter that can
|
|
108
|
+
* drive one, which today is the Responses adapter's ChatGPT target. For
|
|
109
|
+
* every other kind it stays the same loud refusal it has always been —
|
|
110
|
+
* the caller is told which sign-in it has and which one it needs. */
|
|
111
|
+
export declare function authForProfile(name: string, p: ModelProfile): ProfileAuth;
|
|
112
|
+
/** The reason a profile is unavailable, for the surfaces that print it. */
|
|
113
|
+
export declare function unavailableReason(name: string, p: ModelProfile): string;
|
|
114
|
+
/** A profile is available when a sign-in resolves for it — of EITHER
|
|
115
|
+
* shape: an OAuth-only profile is available, and calling the key-only
|
|
116
|
+
* resolver here would have declared it broken. */
|
|
86
117
|
export declare function profileAvailable(p: ModelProfile): boolean;
|
|
87
|
-
/** The resolved runtime model — what the adapter is built from.
|
|
118
|
+
/** The resolved runtime model — what the adapter is built from. Exactly
|
|
119
|
+
* one of the two sign-in fields is set: an OAuth profile has no key to
|
|
120
|
+
* hand over, and the caller must build the adapter from the token thunk
|
|
121
|
+
* instead (the compiler enforces the branch — `apiKey` is optional). */
|
|
88
122
|
export interface ResolvedModel {
|
|
89
123
|
readonly name: string;
|
|
90
124
|
readonly profile: ModelProfile;
|
|
91
|
-
readonly apiKey
|
|
125
|
+
readonly apiKey?: string;
|
|
126
|
+
/** OR-1: the provider whose stored OAuth sign-in this profile uses. */
|
|
127
|
+
readonly oauthProviderId?: string;
|
|
92
128
|
}
|
|
93
129
|
/** "provider/model" direct write → a profile. */
|
|
94
130
|
export declare function directWriteProfile(value: string): ModelProfile | null;
|
package/dist/config.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Schema v1:
|
|
10
10
|
* model?: string — a profile NAME (models.<name>) or "provider/model"
|
|
11
|
-
* direct write (provider: "openai-compat" |
|
|
12
|
-
*
|
|
11
|
+
* direct write (provider: "openai-compat" |
|
|
12
|
+
* "anthropic" | "openai-responses")
|
|
13
|
+
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
13
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
14
15
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
16
|
* contextWindow?: number — tokens
|
|
@@ -29,7 +30,9 @@
|
|
|
29
30
|
import { readFileSync } from "node:fs";
|
|
30
31
|
import { join } from "node:path";
|
|
31
32
|
import { kisoHome } from "./state.js";
|
|
32
|
-
|
|
33
|
+
import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
34
|
+
import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
|
|
35
|
+
const KINDS = ["openai-compat", "anthropic", "openai-responses"];
|
|
33
36
|
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass"];
|
|
34
37
|
export class ConfigError extends Error {
|
|
35
38
|
}
|
|
@@ -177,11 +180,81 @@ export function mergeConfigs(user, project) {
|
|
|
177
180
|
...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
|
-
/**
|
|
183
|
+
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
184
|
+
* plan's resolve rule: a stored credential OWNS the provider (an
|
|
185
|
+
* unusable stored one is an error, never a silent fall back to env);
|
|
186
|
+
* with nothing stored, the profile's env var applies; a keyless profile
|
|
187
|
+
* (no apiKeyEnv — PH-1c, finding PH-F19) is an unauthenticated endpoint.
|
|
188
|
+
*
|
|
189
|
+
* OR-1: a stored OAuth credential is USABLE only by an adapter that can
|
|
190
|
+
* drive one, which today is the Responses adapter's ChatGPT target. For
|
|
191
|
+
* every other kind it stays the same loud refusal it has always been —
|
|
192
|
+
* the caller is told which sign-in it has and which one it needs. */
|
|
193
|
+
export function authForProfile(name, p) {
|
|
194
|
+
const providerId = providerIdOf(p.kind, p.baseUrl);
|
|
195
|
+
if (providerId !== null) {
|
|
196
|
+
let stored;
|
|
197
|
+
try {
|
|
198
|
+
stored = getCredential(providerId);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (err instanceof AuthError)
|
|
202
|
+
throw new ConfigError(`model ${name}: ${err.message}`);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
if (stored !== undefined) {
|
|
206
|
+
if (stored.type === "api-key")
|
|
207
|
+
return { type: "api-key", apiKey: stored.key, source: "store" };
|
|
208
|
+
if (p.kind === "openai-responses")
|
|
209
|
+
return { type: "oauth", providerId };
|
|
210
|
+
throw new ConfigError(`model ${name}: signed in to ${providerId} with OAuth, but this profile's adapter needs an API key — run \`kiso login ${providerId}\` with a key, or \`kiso logout ${providerId}\` to use the env var ${p.apiKeyEnv ?? "(none configured)"}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// OR-2 (2026-09-09): a provider whose manifest takes NO API key (the ChatGPT
|
|
214
|
+
// backend: authMethods ["oauth"]) is never an "unauthenticated endpoint" —
|
|
215
|
+
// with nothing stored it is a profile waiting for its sign-in. Without this
|
|
216
|
+
// the keyless rule below handed it a placeholder key, `/model` said
|
|
217
|
+
// "available", and the adapter (an apiKey selects the first-party target)
|
|
218
|
+
// would have sent an unauthenticated request to the wrong URL.
|
|
219
|
+
if (providerId !== null && !manifestTakesKey(providerId)) {
|
|
220
|
+
throw new ConfigError(`model ${name}: unavailable — not signed in: run \`kiso login ${providerId}\` (this provider takes no API key)`);
|
|
221
|
+
}
|
|
222
|
+
if (p.apiKeyEnv === undefined)
|
|
223
|
+
return { type: "api-key", apiKey: "none", source: "none" };
|
|
224
|
+
const fromEnv = process.env[p.apiKeyEnv];
|
|
225
|
+
if (fromEnv !== undefined)
|
|
226
|
+
return { type: "api-key", apiKey: fromEnv, source: "env" };
|
|
227
|
+
const hint = providerId !== null ? `run \`kiso login ${providerId}\` or set the env var ${p.apiKeyEnv}` : `set the env var ${p.apiKeyEnv}`;
|
|
228
|
+
throw new ConfigError(`model ${name}: unavailable — no credential: ${hint} (configs never store keys, only the env-var name)`);
|
|
229
|
+
}
|
|
230
|
+
/** OR-2: does the provider's manifest admit an API key (or no auth at all)?
|
|
231
|
+
* An unknown id answers yes — the keyless-endpoint rule stays for custom
|
|
232
|
+
* origins; only a manifest that says OAuth-only takes the sign-in path. */
|
|
233
|
+
function manifestTakesKey(providerId) {
|
|
234
|
+
const m = BUILTIN_MANIFESTS.find((x) => x.id === providerId);
|
|
235
|
+
return m === undefined || m.authMethods.includes("api-key") || m.authMethods.includes("none");
|
|
236
|
+
}
|
|
237
|
+
/** The reason a profile is unavailable, for the surfaces that print it. */
|
|
238
|
+
export function unavailableReason(name, p) {
|
|
239
|
+
try {
|
|
240
|
+
authForProfile(name, p);
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return err instanceof Error ? err.message : String(err);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** A profile is available when a sign-in resolves for it — of EITHER
|
|
248
|
+
* shape: an OAuth-only profile is available, and calling the key-only
|
|
249
|
+
* resolver here would have declared it broken. */
|
|
181
250
|
export function profileAvailable(p) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
251
|
+
try {
|
|
252
|
+
authForProfile("?", p);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
185
258
|
}
|
|
186
259
|
/** "provider/model" direct write → a profile. */
|
|
187
260
|
export function directWriteProfile(value) {
|
|
@@ -189,11 +262,15 @@ export function directWriteProfile(value) {
|
|
|
189
262
|
if (slash <= 0 || slash === value.length - 1)
|
|
190
263
|
return null;
|
|
191
264
|
const kind = value.slice(0, slash);
|
|
192
|
-
if (kind !== "openai-compat" && kind !== "anthropic")
|
|
265
|
+
if (kind !== "openai-compat" && kind !== "anthropic" && kind !== "openai-responses")
|
|
193
266
|
return null;
|
|
194
267
|
return {
|
|
195
268
|
kind,
|
|
196
269
|
model: value.slice(slash + 1),
|
|
270
|
+
// OR-1: a direct-write `openai-responses/…` names no baseUrl, so it
|
|
271
|
+
// is the FIRST-PARTY target and its env var is OpenAI's. The
|
|
272
|
+
// ChatGPT backend needs a baseUrl and a stored sign-in, which is a
|
|
273
|
+
// configured profile's job, not a one-line direct write.
|
|
197
274
|
apiKeyEnv: kind === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY",
|
|
198
275
|
};
|
|
199
276
|
}
|
|
@@ -256,12 +333,10 @@ export function resolveModel(modelFlag, merged) {
|
|
|
256
333
|
return null;
|
|
257
334
|
}
|
|
258
335
|
function resolveProfile(name, p) {
|
|
259
|
-
if (!profileAvailable(p)) {
|
|
260
|
-
throw new ConfigError(`model ${name}: unavailable — the env var ${p.apiKeyEnv} is not set (configs never store keys, only the env-var name)`);
|
|
261
|
-
}
|
|
262
336
|
// PH-1c (finding PH-F19): a keyless profile hands the adapter a
|
|
263
337
|
// placeholder — the SDKs require SOME string; the endpoint ignores it.
|
|
264
|
-
|
|
338
|
+
const auth = authForProfile(name, p);
|
|
339
|
+
return auth.type === "oauth" ? { name, profile: p, oauthProviderId: auth.providerId } : { name, profile: p, apiKey: auth.apiKey };
|
|
265
340
|
}
|
|
266
341
|
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
267
342
|
* main before this runs (flags are the top of the chain). */
|
package/dist/dispatch.js
CHANGED
|
@@ -5,11 +5,74 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, palette } from "@vincemakes/kiso-tui";
|
|
7
7
|
import { newSessionId } from "./session-id.js";
|
|
8
|
-
import { buildAdapter, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
8
|
+
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
9
9
|
import { MODES, MODE_NOTE, getMode, setMode } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
|
-
import { agentModel, body, bodyLog, configModels, dock, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName, setLastBinding } from "./state.js";
|
|
12
|
-
import { directWriteProfile, profileAvailable } from "./config.js";
|
|
11
|
+
import { agentModel, body, bodyLog, configModels, dock, lastBinding, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName, setLastBinding } from "./state.js";
|
|
12
|
+
import { authForProfile, directWriteProfile, profileAvailable, unavailableReason } from "./config.js";
|
|
13
|
+
/** The picker's CLI half (owner 2026-09-08): a profile's LEGAL effort levels
|
|
14
|
+
* and its default, from the registry, shown wherever the profile is listed
|
|
15
|
+
* — kiso never silently drops a level, and now it shows the ones it takes.
|
|
16
|
+
* A model the registry does not know shows `unknown` (nothing is guessed);
|
|
17
|
+
* it must not read `none`, which is a REAL level since the Responses rows. */
|
|
18
|
+
function effortNote(p) {
|
|
19
|
+
const reasoning = lookupModelMetadata(p.model, p.baseUrl)?.capabilities.reasoning ?? null;
|
|
20
|
+
const effort = reasoning?.effort ?? null;
|
|
21
|
+
if (effort === null)
|
|
22
|
+
return "effort: unknown";
|
|
23
|
+
return `effort: ${effort.levels.map((l) => (l === effort.default ? `[${l}]` : l)).join(" · ")}`;
|
|
24
|
+
}
|
|
25
|
+
/** OR-7 — the pick panel's second axis, as DATA. `effortNote` above stays
|
|
26
|
+
* the PRINTED list's string (a machine-readable surface whose bytes do
|
|
27
|
+
* not move); the panel gets the levels structured so its cursor can walk
|
|
28
|
+
* them, and so the bracket can mean "the cursor is here" rather than
|
|
29
|
+
* "the registry default".
|
|
30
|
+
*
|
|
31
|
+
* Where the cursor starts: the session's CURRENT effort when this model
|
|
32
|
+
* has that level, else the model's default, else nowhere — a row whose
|
|
33
|
+
* registry default is null (the model page states none) marks nothing,
|
|
34
|
+
* and enter on it applies the profile alone. Nothing is invented. */
|
|
35
|
+
function effortAxis(p) {
|
|
36
|
+
const reasoning = lookupModelMetadata(p.model, p.baseUrl)?.capabilities.reasoning ?? null;
|
|
37
|
+
if (reasoning === null)
|
|
38
|
+
return {};
|
|
39
|
+
const effort = reasoning.effort;
|
|
40
|
+
if (effort === null)
|
|
41
|
+
return {};
|
|
42
|
+
const levels = effort.levels;
|
|
43
|
+
const carried = lastBinding()?.reasoning?.effort;
|
|
44
|
+
const carriedIdx = carried === undefined || carried === "default" ? -1 : levels.indexOf(carried);
|
|
45
|
+
const defaultIdx = effort.default === null ? -1 : levels.indexOf(effort.default);
|
|
46
|
+
const level = carriedIdx >= 0 ? carriedIdx : defaultIdx;
|
|
47
|
+
// the forbidden set is per THINKING mode, and the panel applies its
|
|
48
|
+
// pick as thinking "default" — the same setting `/model <p> <e>`
|
|
49
|
+
// builds — so only pairs forbidden at that mode are dimmed here.
|
|
50
|
+
const disabled = (reasoning.forbidden ?? []).filter((f) => f.thinking === "default").map((f) => levels.indexOf(f.effort)).filter((i) => i >= 0);
|
|
51
|
+
return {
|
|
52
|
+
levels,
|
|
53
|
+
...(level >= 0 ? { level } : {}),
|
|
54
|
+
...(disabled.length > 0 ? { disabled } : {}),
|
|
55
|
+
// the sentence the coordination note asked for: said only when the
|
|
56
|
+
// cursor could NOT land where the previous selection asked.
|
|
57
|
+
...(carried !== undefined && carried !== "default" && carriedIdx < 0 && defaultIdx >= 0
|
|
58
|
+
? { levelNote: `effort ${carried} \u2192 ${levels[defaultIdx]}: the nearest this model supports` }
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** What signs a profile in, for the listing: the env var's name, `oauth`
|
|
63
|
+
* for a subscription sign-in, or `no key` for an unauthenticated endpoint. */
|
|
64
|
+
function signInNote(p) {
|
|
65
|
+
try {
|
|
66
|
+
const auth = authForProfile("?", p);
|
|
67
|
+
if (auth.type === "oauth")
|
|
68
|
+
return "oauth";
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// unavailable — the availability mark says so; the env name still names what would sign it in
|
|
72
|
+
}
|
|
73
|
+
return p.apiKeyEnv ?? "no key";
|
|
74
|
+
}
|
|
75
|
+
import { oauthTokenThunk } from "./auth/token.js";
|
|
13
76
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
14
77
|
* replay routes through it too — a queued "/last" must never become a
|
|
15
78
|
* user turn (v2c: the rl lives in main, so lines arrive earlier and the
|
|
@@ -290,13 +353,17 @@ export function dispatch(line, ctx) {
|
|
|
290
353
|
// surface and this round moves no bytes on one.
|
|
291
354
|
if (arg === "" && dock.active && ctx.input.panelAsk !== undefined) {
|
|
292
355
|
const names = Object.keys(configModels);
|
|
356
|
+
let pickedLevel;
|
|
293
357
|
const picked = await new Promise((resolve) => {
|
|
294
358
|
ctx.input.panelAsk(modelPickView({
|
|
295
359
|
header: `model — current: ${agentModel}`,
|
|
296
360
|
options: names.map((name) => {
|
|
297
361
|
const profile = configModels[name];
|
|
362
|
+
// the note keeps what tells two rows apart; the levels
|
|
363
|
+
// left it for the axis below (they were being cut off
|
|
364
|
+
// the end of the note column at 100 columns).
|
|
298
365
|
const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : [])];
|
|
299
|
-
return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · ") };
|
|
366
|
+
return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · "), ...effortAxis(profile) };
|
|
300
367
|
}),
|
|
301
368
|
// PH-1a (finding PH-F4): the example must be a syntax
|
|
302
369
|
// directWriteProfile actually ACCEPTS — the old
|
|
@@ -311,14 +378,30 @@ export function dispatch(line, ctx) {
|
|
|
311
378
|
// DC-12 (design §4): a panel WAITING ON A HUMAN says ❯. The
|
|
312
379
|
// else-branch keeps ▸ — it names the current tier, which is
|
|
313
380
|
// what ▸ means everywhere else.
|
|
314
|
-
ctx.isRunning() ? "❯ run paused" : `▸ ${getMode()}`), (v) =>
|
|
381
|
+
ctx.isRunning() ? "❯ run paused" : `▸ ${getMode()}`), (v) => {
|
|
382
|
+
// OR-7: the level rides beside the result, so a pick
|
|
383
|
+
// with no second axis is byte-for-byte the old one.
|
|
384
|
+
if (v.action === "picked")
|
|
385
|
+
pickedLevel = v.level;
|
|
386
|
+
resolve(v.action === "picked" ? v.result : null);
|
|
387
|
+
});
|
|
315
388
|
});
|
|
316
389
|
ctx.paintIdle();
|
|
317
390
|
if (picked === null) {
|
|
318
391
|
ctx.input.prompt();
|
|
319
392
|
return; // esc — nothing switched, nothing said
|
|
320
393
|
}
|
|
321
|
-
|
|
394
|
+
if ("index" in picked) {
|
|
395
|
+
// OR-7: the panel hands back both axes and the command
|
|
396
|
+
// string is where they meet — everything below is the
|
|
397
|
+
// typed `/model <profile> <effort>` path, unchanged, so
|
|
398
|
+
// the refusal wording and the durable revision are its.
|
|
399
|
+
const name = names[picked.index];
|
|
400
|
+
const level = pickedLevel === undefined ? undefined : effortAxis(configModels[name]).levels?.[pickedLevel];
|
|
401
|
+
arg = level === undefined ? name : `${name} ${level}`;
|
|
402
|
+
}
|
|
403
|
+
else
|
|
404
|
+
arg = picked.custom;
|
|
322
405
|
}
|
|
323
406
|
if (arg === "") {
|
|
324
407
|
bodyLog(`model: ${agentModel}`);
|
|
@@ -329,7 +412,7 @@ export function dispatch(line, ctx) {
|
|
|
329
412
|
else {
|
|
330
413
|
for (const name of names) {
|
|
331
414
|
const p = configModels[name];
|
|
332
|
-
bodyLog(` ${name} → ${p.kind}/${p.model} · ${p
|
|
415
|
+
bodyLog(` ${name} → ${p.kind}/${p.model} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
|
|
333
416
|
}
|
|
334
417
|
}
|
|
335
418
|
bodyLog("switch with /model <profile-name|provider/model>");
|
|
@@ -347,13 +430,25 @@ export function dispatch(line, ctx) {
|
|
|
347
430
|
bodyLog(`no such model profile: ${profName}`);
|
|
348
431
|
}
|
|
349
432
|
else if (!profileAvailable(profile)) {
|
|
350
|
-
|
|
433
|
+
// the PROFILE name, not the whole argument: what is
|
|
434
|
+
// unavailable is the credential, and an effort token
|
|
435
|
+
// riding along ("claude high: unavailable") reads as
|
|
436
|
+
// though the level were part of the problem. The panel
|
|
437
|
+
// began producing that token in OR-7; the typed path
|
|
438
|
+
// could always produce it too.
|
|
439
|
+
bodyLog(unavailableReason(profName, profile));
|
|
351
440
|
}
|
|
352
441
|
else {
|
|
442
|
+
const auth = authForProfile(profName, profile);
|
|
353
443
|
const adapter = await buildAdapter(profile.kind, {
|
|
354
444
|
// PH-1c (PH-F19): a keyless profile = an unauthenticated
|
|
355
445
|
// endpoint — the placeholder satisfies the SDK's ctor.
|
|
356
|
-
|
|
446
|
+
// OR-1: an OAuth profile passes a token thunk instead
|
|
447
|
+
// — there is no key, and the adapter re-resolves the
|
|
448
|
+
// token per request.
|
|
449
|
+
...(auth.type === "oauth"
|
|
450
|
+
? { oauth: oauthTokenThunk(auth.providerId), ...(ctx.session.id !== undefined ? { promptCacheKey: ctx.session.id } : {}) }
|
|
451
|
+
: { apiKey: auth.apiKey }),
|
|
357
452
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
358
453
|
...(profile.promptCaching !== undefined ? { promptCaching: profile.promptCaching } : {}),
|
|
359
454
|
});
|
|
@@ -370,7 +465,9 @@ export function dispatch(line, ctx) {
|
|
|
370
465
|
let refused = null;
|
|
371
466
|
if (effortTok !== undefined) {
|
|
372
467
|
const candidate = { thinking: "default", effort: effortTok };
|
|
373
|
-
|
|
468
|
+
// OR-1: the endpoint narrows the row — the same model id answers
|
|
469
|
+
// differently at the first-party API and at the subscription backend
|
|
470
|
+
const resolved = resolveReasoning(profile.model, candidate, profile.baseUrl);
|
|
374
471
|
if (resolved.ok)
|
|
375
472
|
reasoning = candidate;
|
|
376
473
|
else
|
|
@@ -386,13 +483,22 @@ export function dispatch(line, ctx) {
|
|
|
386
483
|
adapter,
|
|
387
484
|
model: profile.model,
|
|
388
485
|
provider: profile.kind,
|
|
486
|
+
// OR-1: the endpoint is the fourth passenger — the cost
|
|
487
|
+
// path and the window lookup key on (model, endpoint).
|
|
488
|
+
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
389
489
|
...(scope !== undefined ? { scope } : {}),
|
|
390
490
|
...(reasoning !== undefined ? { reasoning } : {}),
|
|
391
491
|
};
|
|
392
492
|
ctx.session.setModelBinding(binding);
|
|
393
493
|
setLastBinding(binding);
|
|
394
|
-
setAgentModel(profile.model);
|
|
494
|
+
setAgentModel(profile.model, profile.baseUrl);
|
|
395
495
|
setCurrentModelName(arg);
|
|
496
|
+
// OR-7 (owner, 2026-09-09): the status row is repainted BY the
|
|
497
|
+
// switch — it used to keep the old model until the next
|
|
498
|
+
// recap repainted it, so "takes effect on the next turn"
|
|
499
|
+
// read as "did not take". Painted BEFORE the notice so the
|
|
500
|
+
// frame that carries the notice already carries the row.
|
|
501
|
+
ctx.paintIdle();
|
|
396
502
|
body.notice(`model → ${profName} (${profile.model}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
|
|
397
503
|
}
|
|
398
504
|
}
|