@xfey/tutti 0.1.24 → 0.1.26
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/README.md +1 -1
- package/dist/providers/openai/credential-validation.d.ts +24 -0
- package/dist/providers/openai/credential-validation.js +68 -0
- package/dist/server-shell/cli/cli.js +17 -0
- package/dist/server-shell/cli/provider-tui.d.ts +5 -1
- package/dist/server-shell/cli/provider-tui.js +153 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
- packaged / production `tutti launch` 默认连接 `https://tutti.now`;`TUTTI_RELAY_URL` 可覆盖 Relay URL,本地开发 dev runner 默认使用 `http://127.0.0.1:4370`。
|
|
44
44
|
- host 注册使用 machine-local `host_registration_secret`;该 secret 明文只可写入受限权限的 machine-local secret store,不写入目标项目 repo、Git local config 项目身份、SQLite 协作真相或日志,`binding.json` 只保存引用。
|
|
45
45
|
- Relay registration 成功后只把 `relay_project_ref` 写回 machine-local binding;join URL 只在 host 本机终端或当前 host-local control 内存状态中展示,不写入 binding、runtime endpoint、SQLite 协作真相或日志。
|
|
46
|
-
- 普通 `tutti launch` 默认后台运行 host,CLI 完成页展示 join URL
|
|
46
|
+
- 普通 `tutti launch` 默认后台运行 host,CLI 完成页展示 join URL 后直接退出;`tutti launch --foreground` 保留前台诊断模式。Host HTTP request log 与 Control Plane / Run Pipeline runtime log 写入 `TUTTI_HOME/logs/host.log`,不写入目标项目 repo 或常规 CLI stdout。
|
|
47
47
|
- Reference 和 Skills 上传的最终写入由 Host Project API 完成;Host 只通过 Relay host-control resolve 获取短期 R2 URL 下载 staged object,不接收浏览器 base64 文件正文,也不持久化 presigned URL。
|
|
48
48
|
- Host Project API command 的 browser session context 只信任 Relay tunnel metadata 中的 `relay_session_context`;route handler 不读取浏览器身份 header,也不接受 payload 内身份字段。
|
|
49
49
|
- Fastify request log、debug log、SSE payload、activity event 和 run result 都必须经过 redaction,不得泄露 provider secret、host registration secret、host connection token、join token、cookie、host 绝对路径或 run workspace path。
|
|
@@ -34,6 +34,27 @@ export type OpenAiCredentialValidationClient = {
|
|
|
34
34
|
}) => Promise<unknown>;
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
|
+
export type OpenAiModelDiscoveryInput = {
|
|
38
|
+
apiKey: string;
|
|
39
|
+
apiBaseUrl?: string;
|
|
40
|
+
organizationId?: string;
|
|
41
|
+
openAiProjectId?: string;
|
|
42
|
+
};
|
|
43
|
+
export type OpenAiModelDiscoveryClient = {
|
|
44
|
+
models: {
|
|
45
|
+
list: (options?: {
|
|
46
|
+
timeout?: number;
|
|
47
|
+
maxRetries?: number;
|
|
48
|
+
}) => Promise<unknown>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export type OpenAiModelDiscoveryResult = {
|
|
52
|
+
kind: "ok";
|
|
53
|
+
models: string[];
|
|
54
|
+
} | {
|
|
55
|
+
kind: "unavailable";
|
|
56
|
+
reason: "model_list_unavailable" | "model_list_empty";
|
|
57
|
+
};
|
|
37
58
|
export type ValidateOpenAiCredentialOptions = {
|
|
38
59
|
client?: OpenAiCredentialValidationClient;
|
|
39
60
|
now?: () => Date;
|
|
@@ -42,4 +63,7 @@ export declare function summarizeOpenAiCredentialValidationError(error: unknown,
|
|
|
42
63
|
export declare function classifyOpenAiCredentialValidationError(error: unknown): OpenAiCredentialValidationFailureReason;
|
|
43
64
|
export declare function isOpenAiCredentialValidationFailureRetryable(reason: OpenAiCredentialValidationFailureReason): boolean;
|
|
44
65
|
export declare function validateOpenAiCredential(input: OpenAiCredentialValidationInput, options?: ValidateOpenAiCredentialOptions): Promise<OpenAiCredentialValidationResult>;
|
|
66
|
+
export declare function discoverOpenAiModels(input: OpenAiModelDiscoveryInput, options?: {
|
|
67
|
+
client?: OpenAiModelDiscoveryClient;
|
|
68
|
+
}): Promise<OpenAiModelDiscoveryResult>;
|
|
45
69
|
//# sourceMappingURL=credential-validation.d.ts.map
|
|
@@ -2,6 +2,7 @@ import OpenAI, { APIConnectionError, APIConnectionTimeoutError, APIError, Authen
|
|
|
2
2
|
import { DEFAULT_OPENAI_MODEL } from "./model-config.js";
|
|
3
3
|
const VALIDATION_INPUT = "Tutti provider validation. Reply with the single word ok.";
|
|
4
4
|
const VALIDATION_TIMEOUT_MS = 20_000;
|
|
5
|
+
const MODEL_DISCOVERY_TIMEOUT_MS = 12_000;
|
|
5
6
|
function createOpenAiValidationClient(input) {
|
|
6
7
|
const clientOptions = {
|
|
7
8
|
apiKey: input.apiKey,
|
|
@@ -19,6 +20,47 @@ function createOpenAiValidationClient(input) {
|
|
|
19
20
|
}
|
|
20
21
|
return new OpenAI(clientOptions);
|
|
21
22
|
}
|
|
23
|
+
function createOpenAiModelDiscoveryClient(input) {
|
|
24
|
+
const clientOptions = {
|
|
25
|
+
apiKey: input.apiKey,
|
|
26
|
+
maxRetries: 0,
|
|
27
|
+
timeout: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
28
|
+
};
|
|
29
|
+
if (input.apiBaseUrl !== undefined) {
|
|
30
|
+
clientOptions.baseURL = input.apiBaseUrl;
|
|
31
|
+
}
|
|
32
|
+
if (input.organizationId !== undefined) {
|
|
33
|
+
clientOptions.organization = input.organizationId;
|
|
34
|
+
}
|
|
35
|
+
if (input.openAiProjectId !== undefined) {
|
|
36
|
+
clientOptions.project = input.openAiProjectId;
|
|
37
|
+
}
|
|
38
|
+
return new OpenAI(clientOptions);
|
|
39
|
+
}
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === "object" && value !== null;
|
|
42
|
+
}
|
|
43
|
+
function modelIdFromItem(item) {
|
|
44
|
+
if (!isRecord(item)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const id = item.id;
|
|
48
|
+
if (typeof id === "string" && id.trim() !== "") {
|
|
49
|
+
return id.trim();
|
|
50
|
+
}
|
|
51
|
+
const name = item.name;
|
|
52
|
+
if (typeof name === "string" && name.trim() !== "") {
|
|
53
|
+
return name.trim();
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function modelIdsFromResponse(response) {
|
|
58
|
+
if (!isRecord(response) || !Array.isArray(response.data)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(response.data.map(modelIdFromItem).filter((model) => model !== undefined))]
|
|
62
|
+
.sort((left, right) => left.localeCompare(right));
|
|
63
|
+
}
|
|
22
64
|
function lowerErrorText(error) {
|
|
23
65
|
if (error instanceof Error) {
|
|
24
66
|
return error.message.toLowerCase();
|
|
@@ -187,4 +229,30 @@ export async function validateOpenAiCredential(input, options = {}) {
|
|
|
187
229
|
};
|
|
188
230
|
}
|
|
189
231
|
}
|
|
232
|
+
export async function discoverOpenAiModels(input, options = {}) {
|
|
233
|
+
const client = options.client ?? createOpenAiModelDiscoveryClient(input);
|
|
234
|
+
try {
|
|
235
|
+
const response = await client.models.list({
|
|
236
|
+
timeout: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
237
|
+
maxRetries: 0,
|
|
238
|
+
});
|
|
239
|
+
const models = modelIdsFromResponse(response);
|
|
240
|
+
if (models.length === 0) {
|
|
241
|
+
return {
|
|
242
|
+
kind: "unavailable",
|
|
243
|
+
reason: "model_list_empty",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
kind: "ok",
|
|
248
|
+
models,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return {
|
|
253
|
+
kind: "unavailable",
|
|
254
|
+
reason: "model_list_unavailable",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
190
258
|
//# sourceMappingURL=credential-validation.js.map
|
|
@@ -91,6 +91,22 @@ function helpForTopic(topic) {
|
|
|
91
91
|
}
|
|
92
92
|
return HELP_TEXT;
|
|
93
93
|
}
|
|
94
|
+
function restoreInteractiveInput() {
|
|
95
|
+
if (!process.stdin.isTTY) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (process.stdin.isRaw === true) {
|
|
99
|
+
process.stdin.setRawMode(false);
|
|
100
|
+
}
|
|
101
|
+
process.stdin.removeAllListeners("keypress");
|
|
102
|
+
process.stdin.pause();
|
|
103
|
+
}
|
|
104
|
+
function exitAfterBackgroundLaunch() {
|
|
105
|
+
restoreInteractiveInput();
|
|
106
|
+
process.stdout.write("\n", () => {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
94
110
|
async function runProviderSetupCommand(target) {
|
|
95
111
|
const project = resolveExistingProjectContext({
|
|
96
112
|
...(target === undefined ? {} : { target }),
|
|
@@ -123,6 +139,7 @@ try {
|
|
|
123
139
|
}
|
|
124
140
|
else {
|
|
125
141
|
await runBackgroundLaunchCommand(command);
|
|
142
|
+
exitAfterBackgroundLaunch();
|
|
126
143
|
}
|
|
127
144
|
break;
|
|
128
145
|
case "internal-host-run":
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { type ConfigureProjectOpenAiProviderResult } from "../../providers/openai/index.js";
|
|
2
2
|
import type { ProjectId } from "@tutti/shared/ids";
|
|
3
|
-
type ProviderSetupStep = "base-url" | "api-key";
|
|
3
|
+
type ProviderSetupStep = "base-url" | "api-key" | "model";
|
|
4
4
|
type ProviderFormState = {
|
|
5
5
|
step: ProviderSetupStep;
|
|
6
6
|
baseUrl: string;
|
|
7
7
|
apiKey: string;
|
|
8
|
+
modelOptions?: string[];
|
|
9
|
+
modelListStatus?: "loaded" | "unavailable";
|
|
10
|
+
selectedModelIndex?: number;
|
|
11
|
+
customModel?: string;
|
|
8
12
|
error: string | undefined;
|
|
9
13
|
};
|
|
10
14
|
type ProviderSetupLogEvent = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emitKeypressEvents } from "node:readline";
|
|
2
2
|
import { redactError } from "@tutti/shared/utils";
|
|
3
|
-
import { configureProjectOpenAiProvider, DEFAULT_OPENAI_MODEL, } from "../../providers/openai/index.js";
|
|
3
|
+
import { configureProjectOpenAiProvider, DEFAULT_OPENAI_MODEL, discoverOpenAiModels, } from "../../providers/openai/index.js";
|
|
4
4
|
import { LaunchError } from "./errors.js";
|
|
5
5
|
import { appendHostLogLine, getHostLogFilePath } from "./machine-local.js";
|
|
6
6
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
@@ -10,6 +10,7 @@ const BLINK_ON = "\u001B[5m";
|
|
|
10
10
|
const BLINK_OFF = "\u001B[25m";
|
|
11
11
|
const CLEAR = "\u001B[2J\u001B[H";
|
|
12
12
|
const FIELD_BOX_WIDTH = 60;
|
|
13
|
+
const MODEL_LIST_WINDOW = 8;
|
|
13
14
|
function providerSetupCancelled() {
|
|
14
15
|
return new LaunchError("provider_setup_cancelled", "Provider setup cancelled.", "Run the command again when you are ready.");
|
|
15
16
|
}
|
|
@@ -25,10 +26,50 @@ function renderFieldInput(options) {
|
|
|
25
26
|
`+${"-".repeat(FIELD_BOX_WIDTH + 2)}+`,
|
|
26
27
|
];
|
|
27
28
|
}
|
|
29
|
+
function modelOptionsWithCustom(state) {
|
|
30
|
+
return [...(state.modelOptions ?? []), ""];
|
|
31
|
+
}
|
|
32
|
+
function selectedModelIndex(state) {
|
|
33
|
+
const options = modelOptionsWithCustom(state);
|
|
34
|
+
const selected = state.selectedModelIndex ?? 0;
|
|
35
|
+
return Math.min(Math.max(0, selected), Math.max(0, options.length - 1));
|
|
36
|
+
}
|
|
37
|
+
function selectedModelValue(state) {
|
|
38
|
+
const options = modelOptionsWithCustom(state);
|
|
39
|
+
const selected = selectedModelIndex(state);
|
|
40
|
+
if (selected < (state.modelOptions ?? []).length) {
|
|
41
|
+
return options[selected] ?? "";
|
|
42
|
+
}
|
|
43
|
+
return state.customModel?.trim() ?? "";
|
|
44
|
+
}
|
|
45
|
+
function renderModelOptions(state) {
|
|
46
|
+
const options = modelOptionsWithCustom(state);
|
|
47
|
+
const selected = selectedModelIndex(state);
|
|
48
|
+
const modelCount = state.modelOptions?.length ?? 0;
|
|
49
|
+
const windowStart = Math.max(0, Math.min(selected - Math.floor(MODEL_LIST_WINDOW / 2), options.length - MODEL_LIST_WINDOW));
|
|
50
|
+
const windowEnd = Math.min(options.length, windowStart + MODEL_LIST_WINDOW);
|
|
51
|
+
const lines = [];
|
|
52
|
+
if (windowStart > 0) {
|
|
53
|
+
lines.push(" ...");
|
|
54
|
+
}
|
|
55
|
+
for (let index = windowStart; index < windowEnd; index += 1) {
|
|
56
|
+
const marker = index === selected ? ">" : " ";
|
|
57
|
+
const label = index < modelCount ? options[index] : "Custom model";
|
|
58
|
+
const suffix = index < modelCount ? "" : " (type your own)";
|
|
59
|
+
lines.push(`${marker} ${label}${suffix}`);
|
|
60
|
+
}
|
|
61
|
+
if (windowEnd < options.length) {
|
|
62
|
+
lines.push(" ...");
|
|
63
|
+
}
|
|
64
|
+
if (selected === modelCount) {
|
|
65
|
+
lines.push("", ...renderFieldInput({ value: state.customModel ?? "" }));
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
28
69
|
function renderProviderField(state) {
|
|
29
70
|
if (state.step === "base-url") {
|
|
30
71
|
return [
|
|
31
|
-
"Step 1/
|
|
72
|
+
"Step 1/3 Base URL",
|
|
32
73
|
"OpenAI-compatible Responses API base URL; must expose POST /responses.",
|
|
33
74
|
"",
|
|
34
75
|
...renderFieldInput({
|
|
@@ -38,18 +79,30 @@ function renderProviderField(state) {
|
|
|
38
79
|
"[Enter] Continue [Esc] Cancel",
|
|
39
80
|
];
|
|
40
81
|
}
|
|
82
|
+
if (state.step === "api-key") {
|
|
83
|
+
return [
|
|
84
|
+
"Step 2/3 API Key",
|
|
85
|
+
"Stored only in the machine-local Tutti credential store.",
|
|
86
|
+
"",
|
|
87
|
+
...renderFieldInput({
|
|
88
|
+
value: state.apiKey,
|
|
89
|
+
secret: true,
|
|
90
|
+
}),
|
|
91
|
+
"",
|
|
92
|
+
"[Enter] Continue [Esc] Back to Base URL",
|
|
93
|
+
];
|
|
94
|
+
}
|
|
41
95
|
return [
|
|
42
|
-
"Step
|
|
43
|
-
|
|
96
|
+
"Step 3/3 Model",
|
|
97
|
+
state.modelListStatus === "loaded"
|
|
98
|
+
? "Choose a model with Up/Down, or move to Custom model and type one."
|
|
99
|
+
: "Could not read the model list. Type a model name in Custom model.",
|
|
44
100
|
"",
|
|
45
|
-
...
|
|
46
|
-
value: state.apiKey,
|
|
47
|
-
secret: true,
|
|
48
|
-
}),
|
|
101
|
+
...renderModelOptions(state),
|
|
49
102
|
"",
|
|
50
103
|
state.error === undefined
|
|
51
|
-
? "[Enter] Validate [Esc] Back to
|
|
52
|
-
: "[Enter] Retry [Esc] Back to
|
|
104
|
+
? "[Enter] Validate [Esc] Back to API Key"
|
|
105
|
+
: "[Enter] Retry [Esc] Back to API Key",
|
|
53
106
|
];
|
|
54
107
|
}
|
|
55
108
|
export function renderProviderForm(options) {
|
|
@@ -122,6 +175,7 @@ async function readProviderForm(options) {
|
|
|
122
175
|
error: options.initialError,
|
|
123
176
|
};
|
|
124
177
|
return await new Promise((resolve, reject) => {
|
|
178
|
+
let busy = false;
|
|
125
179
|
const render = () => {
|
|
126
180
|
options.stdout.write(renderProviderForm({
|
|
127
181
|
state,
|
|
@@ -135,7 +189,38 @@ async function readProviderForm(options) {
|
|
|
135
189
|
}
|
|
136
190
|
options.stdout.write(SHOW_CURSOR);
|
|
137
191
|
};
|
|
192
|
+
const openModelStep = async () => {
|
|
193
|
+
busy = true;
|
|
194
|
+
state.error = undefined;
|
|
195
|
+
options.stdout.write(renderModelDiscovery(options.stdout.isTTY === true));
|
|
196
|
+
const discoverModels = options.discoverModels ?? discoverOpenAiModels;
|
|
197
|
+
const result = await discoverModels({
|
|
198
|
+
apiKey: state.apiKey.trim(),
|
|
199
|
+
apiBaseUrl: state.baseUrl.trim(),
|
|
200
|
+
}).catch(() => ({
|
|
201
|
+
kind: "unavailable",
|
|
202
|
+
reason: "model_list_unavailable",
|
|
203
|
+
}));
|
|
204
|
+
if (result.kind === "ok") {
|
|
205
|
+
state.modelOptions = result.models;
|
|
206
|
+
state.modelListStatus = "loaded";
|
|
207
|
+
const defaultIndex = result.models.indexOf(DEFAULT_OPENAI_MODEL);
|
|
208
|
+
state.selectedModelIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
state.modelOptions = [];
|
|
212
|
+
state.modelListStatus = "unavailable";
|
|
213
|
+
state.selectedModelIndex = 0;
|
|
214
|
+
}
|
|
215
|
+
state.customModel = "";
|
|
216
|
+
state.step = "model";
|
|
217
|
+
busy = false;
|
|
218
|
+
render();
|
|
219
|
+
};
|
|
138
220
|
const onKeypress = (character, key) => {
|
|
221
|
+
if (busy) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
139
224
|
if (key.ctrl === true && key.name === "c") {
|
|
140
225
|
cleanup();
|
|
141
226
|
reject(providerSetupCancelled());
|
|
@@ -148,6 +233,12 @@ async function readProviderForm(options) {
|
|
|
148
233
|
render();
|
|
149
234
|
return;
|
|
150
235
|
}
|
|
236
|
+
if (state.step === "model") {
|
|
237
|
+
state.step = "api-key";
|
|
238
|
+
state.error = undefined;
|
|
239
|
+
render();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
151
242
|
cleanup();
|
|
152
243
|
reject(providerSetupCancelled());
|
|
153
244
|
return;
|
|
@@ -164,22 +255,48 @@ async function readProviderForm(options) {
|
|
|
164
255
|
render();
|
|
165
256
|
return;
|
|
166
257
|
}
|
|
167
|
-
if (state.
|
|
168
|
-
state.
|
|
258
|
+
if (state.step === "api-key") {
|
|
259
|
+
if (state.apiKey.trim() === "") {
|
|
260
|
+
state.error = "API key is required.";
|
|
261
|
+
render();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
void openModelStep();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const model = selectedModelValue(state);
|
|
268
|
+
if (model === "") {
|
|
269
|
+
state.error = "Model name is required.";
|
|
169
270
|
render();
|
|
170
271
|
return;
|
|
171
272
|
}
|
|
172
273
|
cleanup();
|
|
173
|
-
resolve({
|
|
274
|
+
resolve({
|
|
275
|
+
baseUrl: state.baseUrl.trim(),
|
|
276
|
+
apiKey: state.apiKey.trim(),
|
|
277
|
+
defaultModel: model,
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (state.step === "model" && (key.name === "up" || key.name === "down")) {
|
|
282
|
+
const optionsWithCustom = modelOptionsWithCustom(state);
|
|
283
|
+
const delta = key.name === "up" ? -1 : 1;
|
|
284
|
+
state.selectedModelIndex =
|
|
285
|
+
(selectedModelIndex(state) + delta + optionsWithCustom.length) % optionsWithCustom.length;
|
|
286
|
+
state.error = undefined;
|
|
287
|
+
render();
|
|
174
288
|
return;
|
|
175
289
|
}
|
|
176
290
|
if (key.name === "backspace") {
|
|
177
291
|
if (state.step === "base-url") {
|
|
178
292
|
state.baseUrl = state.baseUrl.slice(0, -1);
|
|
179
293
|
}
|
|
180
|
-
else {
|
|
294
|
+
else if (state.step === "api-key") {
|
|
181
295
|
state.apiKey = state.apiKey.slice(0, -1);
|
|
182
296
|
}
|
|
297
|
+
else if (selectedModelIndex(state) === (state.modelOptions?.length ?? 0)) {
|
|
298
|
+
state.customModel = (state.customModel ?? "").slice(0, -1);
|
|
299
|
+
}
|
|
183
300
|
state.error = undefined;
|
|
184
301
|
render();
|
|
185
302
|
return;
|
|
@@ -188,9 +305,13 @@ async function readProviderForm(options) {
|
|
|
188
305
|
if (state.step === "base-url") {
|
|
189
306
|
state.baseUrl += character;
|
|
190
307
|
}
|
|
191
|
-
else {
|
|
308
|
+
else if (state.step === "api-key") {
|
|
192
309
|
state.apiKey += character;
|
|
193
310
|
}
|
|
311
|
+
else {
|
|
312
|
+
state.selectedModelIndex = state.modelOptions?.length ?? 0;
|
|
313
|
+
state.customModel = `${state.customModel ?? ""}${character}`;
|
|
314
|
+
}
|
|
194
315
|
state.error = undefined;
|
|
195
316
|
render();
|
|
196
317
|
}
|
|
@@ -203,7 +324,19 @@ async function readProviderForm(options) {
|
|
|
203
324
|
render();
|
|
204
325
|
});
|
|
205
326
|
}
|
|
206
|
-
function
|
|
327
|
+
function renderModelDiscovery(tty) {
|
|
328
|
+
return [
|
|
329
|
+
CLEAR,
|
|
330
|
+
HIDE_CURSOR,
|
|
331
|
+
renderTuttiTerminalLogo({ tty }),
|
|
332
|
+
"",
|
|
333
|
+
"Provider setup",
|
|
334
|
+
"Looking up available models for this provider.",
|
|
335
|
+
"",
|
|
336
|
+
"Checking model list...",
|
|
337
|
+
].join("\n");
|
|
338
|
+
}
|
|
339
|
+
function renderChecking(frame, tty, model) {
|
|
207
340
|
const frames = ["|", "/", "-", "\\"];
|
|
208
341
|
const indicator = frames[frame % frames.length] ?? "|";
|
|
209
342
|
return [
|
|
@@ -214,7 +347,7 @@ function renderChecking(frame, tty) {
|
|
|
214
347
|
"Provider setup",
|
|
215
348
|
"Validating the configured Responses API endpoint.",
|
|
216
349
|
"",
|
|
217
|
-
`${indicator} Checking provider connection with ${
|
|
350
|
+
`${indicator} Checking provider connection with ${model}...`,
|
|
218
351
|
].join("\n");
|
|
219
352
|
}
|
|
220
353
|
export async function runProviderSetupTui(options) {
|
|
@@ -241,16 +374,17 @@ export async function runProviderSetupTui(options) {
|
|
|
241
374
|
initialStep = "api-key";
|
|
242
375
|
let frame = 0;
|
|
243
376
|
const interval = setInterval(() => {
|
|
244
|
-
stdout.write(renderChecking(frame, stdout.isTTY === true));
|
|
377
|
+
stdout.write(renderChecking(frame, stdout.isTTY === true, input.defaultModel));
|
|
245
378
|
frame += 1;
|
|
246
379
|
}, 120);
|
|
247
380
|
try {
|
|
248
|
-
stdout.write(renderChecking(frame, stdout.isTTY === true));
|
|
381
|
+
stdout.write(renderChecking(frame, stdout.isTTY === true, input.defaultModel));
|
|
249
382
|
const result = await configureProjectOpenAiProvider({
|
|
250
383
|
tuttiHome: options.tuttiHome,
|
|
251
384
|
projectId: options.projectId,
|
|
252
385
|
apiBaseUrl: input.baseUrl,
|
|
253
386
|
apiKey: input.apiKey,
|
|
387
|
+
defaultModel: input.defaultModel,
|
|
254
388
|
});
|
|
255
389
|
clearInterval(interval);
|
|
256
390
|
stdout.write(SHOW_CURSOR);
|