@xfey/tutti 0.1.79 → 0.1.80
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/index.d.ts +1 -0
- package/dist/providers/openai/index.js +1 -0
- package/dist/providers/openai/machine-local-files.d.ts +1 -1
- package/dist/providers/openai/machine-local-files.js +32 -9
- package/dist/providers/openai/provider-config.d.ts +5 -1
- package/dist/providers/openai/provider-config.js +25 -4
- package/dist/providers/openai/provider-models.d.ts +41 -0
- package/dist/providers/openai/provider-models.js +82 -0
- package/dist/providers/openai/provider-setup.js +5 -2
- package/dist/server-shell/cli/host-server-runtime.d.ts +1 -1
- package/dist/server-shell/cli/host-server-runtime.js +63 -1
- package/dist/server-shell/http/routes/project-api/invalidations.d.ts +1 -0
- package/dist/server-shell/http/routes/project-api/invalidations.js +4 -5
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +38 -14
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.js +26 -1
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +38 -14
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -2
- package/dist/server-shell/http/routes/project-api/schemas.js +2 -0
- package/dist/server-shell/http/routes/project-api/session-requirements.d.ts +1 -0
- package/dist/server-shell/http/routes/project-api/session-requirements.js +7 -0
- package/dist/server-shell/http/routes/project-api/system-routes.js +96 -5
- package/dist/server-shell/http/routes/project-api/types.d.ts +10 -1
- package/dist/server-shell/http/routes/project-api/workspace-projections.js +4 -2
- package/dist/server-shell/http/routes/project-api.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +36 -0
- package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +58 -1
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +9 -1
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BFQnHtRi.js → homepage-motion-scene-DXzmjALa.js} +1 -1
- package/web/assets/{index-C4cdSaSb.js → index-9gApbtPC.js} +3 -3
- package/web/assets/index-BKeSEhsn.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-BUfAlA_H.css +0 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
## 当前状态
|
|
30
30
|
|
|
31
|
-
- 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、当前 runtime version bootstrap projection、idle / package update cleanup、全机readiness、exact npm global install、跨版本worker / recovery与私有日志的机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts
|
|
31
|
+
- 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、当前 runtime version bootstrap projection、idle / package update cleanup、全机readiness、exact npm global install、跨版本worker / recovery与私有日志的机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层、Owner-only provider model discovery / update 和 OpenAI / Codex provider 接入。
|
|
32
32
|
- 新 Host project 的空群聊会由 collaboration-state 幂等写入一条 Tutti 欢迎消息;它不经过 browser message command,不触发 `@tutti`、Chat read-state、Scratchpad 或 Timeline / execution 流程。
|
|
33
33
|
- Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
|
|
34
34
|
- Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
|
|
@@ -8,6 +8,7 @@ export * from "./credential-store.js";
|
|
|
8
8
|
export * from "./credential-validation.js";
|
|
9
9
|
export * from "./model-config.js";
|
|
10
10
|
export * from "./provider-config.js";
|
|
11
|
+
export * from "./provider-models.js";
|
|
11
12
|
export * from "./provider-setup.js";
|
|
12
13
|
export * from "./sdk-procedure-runner.js";
|
|
13
14
|
export * from "./token-usage.js";
|
|
@@ -8,6 +8,7 @@ export * from "./credential-store.js";
|
|
|
8
8
|
export * from "./credential-validation.js";
|
|
9
9
|
export * from "./model-config.js";
|
|
10
10
|
export * from "./provider-config.js";
|
|
11
|
+
export * from "./provider-models.js";
|
|
11
12
|
export * from "./provider-setup.js";
|
|
12
13
|
export * from "./sdk-procedure-runner.js";
|
|
13
14
|
export * from "./token-usage.js";
|
|
@@ -8,7 +8,7 @@ export type JsonFileReadResult = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare function ensurePrivateDirectory(directoryPath: string): void;
|
|
10
10
|
export declare function writePrivateJsonFile(filePath: string, value: unknown): void;
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function writePrivateTextFileAtomically(filePath: string, value: string): void;
|
|
12
12
|
export declare function readJsonFile(filePath: string): JsonFileReadResult;
|
|
13
13
|
export declare function readTextFile(filePath: string): {
|
|
14
14
|
kind: "missing";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
3
4
|
function isErrnoException(error) {
|
|
4
5
|
return error instanceof Error && "code" in error;
|
|
5
6
|
}
|
|
@@ -15,13 +16,35 @@ export function writePrivateJsonFile(filePath, value) {
|
|
|
15
16
|
});
|
|
16
17
|
chmodSync(filePath, 0o600);
|
|
17
18
|
}
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
export function writePrivateTextFileAtomically(filePath, value) {
|
|
20
|
+
const directoryPath = dirname(filePath);
|
|
21
|
+
ensurePrivateDirectory(directoryPath);
|
|
22
|
+
const temporaryPath = join(directoryPath, `.${basename(filePath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
23
|
+
let descriptor;
|
|
24
|
+
try {
|
|
25
|
+
descriptor = openSync(temporaryPath, "wx", 0o600);
|
|
26
|
+
writeFileSync(descriptor, value, { encoding: "utf8" });
|
|
27
|
+
closeSync(descriptor);
|
|
28
|
+
descriptor = undefined;
|
|
29
|
+
renameSync(temporaryPath, filePath);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (descriptor !== undefined) {
|
|
33
|
+
try {
|
|
34
|
+
closeSync(descriptor);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Preserve the original write or close failure.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
unlinkSync(temporaryPath);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// The temporary file may already have been renamed or never created.
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
25
48
|
}
|
|
26
49
|
export function readJsonFile(filePath) {
|
|
27
50
|
let raw;
|
|
@@ -3,7 +3,7 @@ import { type OpenAiCredentialRef } from "./credential-store.js";
|
|
|
3
3
|
import type { OPENAI_API_KEY_AUTH_KIND, OPENAI_PROVIDER } from "./model-config.js";
|
|
4
4
|
import { OpenAiProviderConfigError } from "./provider-config-errors.js";
|
|
5
5
|
export { OpenAiProviderConfigError } from "./provider-config-errors.js";
|
|
6
|
-
export { parseOpenAiProviderConfig
|
|
6
|
+
export { parseOpenAiProviderConfig } from "./provider-config-shape.js";
|
|
7
7
|
export type OpenAiProviderConfig = {
|
|
8
8
|
provider: typeof OPENAI_PROVIDER;
|
|
9
9
|
auth_kind: typeof OPENAI_API_KEY_AUTH_KIND;
|
|
@@ -58,6 +58,10 @@ export type OpenAiProviderConfigResolution = {
|
|
|
58
58
|
};
|
|
59
59
|
export declare function resolveTuttiHome(input: string | undefined, cwd?: string): string;
|
|
60
60
|
export declare function getProjectProviderConfigPath(tuttiHome: string, projectId: ProjectId): string;
|
|
61
|
+
export declare function withProjectOpenAiProviderConfigLock<T>(options: {
|
|
62
|
+
tuttiHome: string;
|
|
63
|
+
projectId: ProjectId;
|
|
64
|
+
}, operation: () => Promise<T>): Promise<T>;
|
|
61
65
|
export declare function readProjectOpenAiProviderConfig(tuttiHome: string, projectId: ProjectId): ProviderConfigReadResult<OpenAiProviderConfig>;
|
|
62
66
|
export declare function resolveOpenAiProviderConfig(options: {
|
|
63
67
|
tuttiHome: string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { openAiCredentialExists
|
|
4
|
-
import { readTextFile,
|
|
3
|
+
import { openAiCredentialExists } from "./credential-store.js";
|
|
4
|
+
import { readTextFile, writePrivateTextFileAtomically } from "./machine-local-files.js";
|
|
5
5
|
import { OpenAiProviderConfigError } from "./provider-config-errors.js";
|
|
6
6
|
import { parseOpenAiProviderConfig } from "./provider-config-shape.js";
|
|
7
7
|
import { formatProjectOpenAiProviderConfig, parseTomlOpenAiSection, parseTuttiConfigToml, } from "./provider-config-toml.js";
|
|
8
8
|
export { OpenAiProviderConfigError } from "./provider-config-errors.js";
|
|
9
|
-
export { parseOpenAiProviderConfig
|
|
9
|
+
export { parseOpenAiProviderConfig } from "./provider-config-shape.js";
|
|
10
10
|
export function resolveTuttiHome(input, cwd = process.cwd()) {
|
|
11
11
|
if (input === undefined || input.trim() === "") {
|
|
12
12
|
return join(homedir(), ".tutti");
|
|
@@ -23,6 +23,27 @@ export function resolveTuttiHome(input, cwd = process.cwd()) {
|
|
|
23
23
|
export function getProjectProviderConfigPath(tuttiHome, projectId) {
|
|
24
24
|
return join(tuttiHome, "projects", projectId, "config.toml");
|
|
25
25
|
}
|
|
26
|
+
const providerConfigMutationTails = new Map();
|
|
27
|
+
export async function withProjectOpenAiProviderConfigLock(options, operation) {
|
|
28
|
+
const key = getProjectProviderConfigPath(options.tuttiHome, options.projectId);
|
|
29
|
+
const previous = providerConfigMutationTails.get(key) ?? Promise.resolve();
|
|
30
|
+
let release = () => { };
|
|
31
|
+
const current = new Promise((resolve) => {
|
|
32
|
+
release = resolve;
|
|
33
|
+
});
|
|
34
|
+
const tail = previous.then(() => current);
|
|
35
|
+
providerConfigMutationTails.set(key, tail);
|
|
36
|
+
await previous;
|
|
37
|
+
try {
|
|
38
|
+
return await operation();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
release();
|
|
42
|
+
if (providerConfigMutationTails.get(key) === tail) {
|
|
43
|
+
providerConfigMutationTails.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
26
47
|
export function readProjectOpenAiProviderConfig(tuttiHome, projectId) {
|
|
27
48
|
const sourcePath = getProjectProviderConfigPath(tuttiHome, projectId);
|
|
28
49
|
const readResult = readTextFile(sourcePath);
|
|
@@ -77,7 +98,7 @@ export function resolveOpenAiProviderConfig(options) {
|
|
|
77
98
|
};
|
|
78
99
|
}
|
|
79
100
|
export function writeProjectOpenAiProviderConfig(tuttiHome, projectId, config) {
|
|
80
|
-
|
|
101
|
+
writePrivateTextFileAtomically(getProjectProviderConfigPath(tuttiHome, projectId), formatProjectOpenAiProviderConfig(config));
|
|
81
102
|
}
|
|
82
103
|
export function createRedactedOpenAiProviderConfigProjection(config) {
|
|
83
104
|
const projection = {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ProjectId } from "@tutti/shared/ids";
|
|
2
|
+
import { type OpenAiCredentialValidationFailureReason, type OpenAiCredentialValidationInput, type OpenAiCredentialValidationResult, type OpenAiModelDiscoveryInput, type OpenAiModelDiscoveryResult } from "./credential-validation.js";
|
|
3
|
+
import { type RedactedOpenAiProviderConfigProjection } from "./provider-config.js";
|
|
4
|
+
export type ProjectOpenAiProviderUnavailableReason = "provider_not_configured" | "provider_config_invalid" | "provider_credential_missing";
|
|
5
|
+
export type ProjectOpenAiModelDiscoveryResult = OpenAiModelDiscoveryResult | {
|
|
6
|
+
kind: "unavailable";
|
|
7
|
+
reason: ProjectOpenAiProviderUnavailableReason;
|
|
8
|
+
};
|
|
9
|
+
export type UpdateProjectOpenAiDefaultModelResult = {
|
|
10
|
+
kind: "updated";
|
|
11
|
+
projection: RedactedOpenAiProviderConfigProjection;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "unchanged";
|
|
14
|
+
projection: RedactedOpenAiProviderConfigProjection;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "stale_default_model";
|
|
17
|
+
current_default_model: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "validation_failed";
|
|
20
|
+
reason: OpenAiCredentialValidationFailureReason;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "provider_unavailable";
|
|
24
|
+
reason: ProjectOpenAiProviderUnavailableReason;
|
|
25
|
+
};
|
|
26
|
+
export type DiscoverProjectOpenAiModelsOptions = {
|
|
27
|
+
tuttiHome: string;
|
|
28
|
+
projectId: ProjectId;
|
|
29
|
+
discoverModels?: (input: OpenAiModelDiscoveryInput) => Promise<OpenAiModelDiscoveryResult>;
|
|
30
|
+
};
|
|
31
|
+
export type UpdateProjectOpenAiDefaultModelOptions = {
|
|
32
|
+
tuttiHome: string;
|
|
33
|
+
projectId: ProjectId;
|
|
34
|
+
defaultModel: string;
|
|
35
|
+
expectedDefaultModel: string;
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
validateCredential?: (input: OpenAiCredentialValidationInput) => Promise<OpenAiCredentialValidationResult>;
|
|
38
|
+
};
|
|
39
|
+
export declare function discoverProjectOpenAiModels(options: DiscoverProjectOpenAiModelsOptions): Promise<ProjectOpenAiModelDiscoveryResult>;
|
|
40
|
+
export declare function updateProjectOpenAiDefaultModel(options: UpdateProjectOpenAiDefaultModelOptions): Promise<UpdateProjectOpenAiDefaultModelResult>;
|
|
41
|
+
//# sourceMappingURL=provider-models.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readOpenAiApiKeyCredential } from "./credential-store.js";
|
|
2
|
+
import { discoverOpenAiModels, validateOpenAiCredential, } from "./credential-validation.js";
|
|
3
|
+
import { createRedactedOpenAiProviderConfigProjection, readProjectOpenAiProviderConfig, withProjectOpenAiProviderConfigLock, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
|
|
4
|
+
function providerConfigSnapshot(options) {
|
|
5
|
+
const config = readProjectOpenAiProviderConfig(options.tuttiHome, options.projectId);
|
|
6
|
+
if (config.kind === "missing") {
|
|
7
|
+
return { kind: "unavailable", reason: "provider_not_configured" };
|
|
8
|
+
}
|
|
9
|
+
if (config.kind === "invalid") {
|
|
10
|
+
return { kind: "unavailable", reason: "provider_config_invalid" };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const credential = readOpenAiApiKeyCredential(options.tuttiHome, options.projectId, config.config.credential_ref);
|
|
14
|
+
return { kind: "ready", config: config.config, apiKey: credential.secret };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { kind: "unavailable", reason: "provider_credential_missing" };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function providerInput(config, apiKey) {
|
|
21
|
+
return {
|
|
22
|
+
apiKey,
|
|
23
|
+
...(config.api_base_url === undefined ? {} : { apiBaseUrl: config.api_base_url }),
|
|
24
|
+
...(config.organization_id === undefined ? {} : { organizationId: config.organization_id }),
|
|
25
|
+
...(config.openai_project_id === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { openAiProjectId: config.openai_project_id }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function discoverProjectOpenAiModels(options) {
|
|
31
|
+
return await withProjectOpenAiProviderConfigLock(options, async () => {
|
|
32
|
+
const snapshot = providerConfigSnapshot(options);
|
|
33
|
+
if (snapshot.kind === "unavailable") {
|
|
34
|
+
return snapshot;
|
|
35
|
+
}
|
|
36
|
+
return await (options.discoverModels ?? discoverOpenAiModels)(providerInput(snapshot.config, snapshot.apiKey));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function updateProjectOpenAiDefaultModel(options) {
|
|
40
|
+
return await withProjectOpenAiProviderConfigLock(options, async () => {
|
|
41
|
+
const snapshot = providerConfigSnapshot(options);
|
|
42
|
+
if (snapshot.kind === "unavailable") {
|
|
43
|
+
return { kind: "provider_unavailable", reason: snapshot.reason };
|
|
44
|
+
}
|
|
45
|
+
if (snapshot.config.default_model !== options.expectedDefaultModel) {
|
|
46
|
+
return {
|
|
47
|
+
kind: "stale_default_model",
|
|
48
|
+
current_default_model: snapshot.config.default_model,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (snapshot.config.default_model === options.defaultModel) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "unchanged",
|
|
54
|
+
projection: createRedactedOpenAiProviderConfigProjection(snapshot.config),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const validator = options.validateCredential ??
|
|
58
|
+
((input) => validateOpenAiCredential(input, options.now === undefined ? {} : { now: options.now }));
|
|
59
|
+
const validation = await validator({
|
|
60
|
+
...providerInput(snapshot.config, snapshot.apiKey),
|
|
61
|
+
defaultModel: options.defaultModel,
|
|
62
|
+
});
|
|
63
|
+
if (validation.kind === "failed") {
|
|
64
|
+
return {
|
|
65
|
+
kind: "validation_failed",
|
|
66
|
+
reason: validation.reason,
|
|
67
|
+
retryable: validation.retryable,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const config = {
|
|
71
|
+
...snapshot.config,
|
|
72
|
+
default_model: validation.default_model,
|
|
73
|
+
validated_at: validation.validated_at,
|
|
74
|
+
};
|
|
75
|
+
writeProjectOpenAiProviderConfig(options.tuttiHome, options.projectId, config);
|
|
76
|
+
return {
|
|
77
|
+
kind: "updated",
|
|
78
|
+
projection: createRedactedOpenAiProviderConfigProjection(config),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=provider-models.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createOpenAiCredentialRef, createRedactedOpenAiApiKeyLabel, isOpenAiApiKey, writeOpenAiApiKeyCredential, } from "./credential-store.js";
|
|
2
2
|
import { validateOpenAiCredential, } from "./credential-validation.js";
|
|
3
3
|
import { DEFAULT_OPENAI_MODEL, OPENAI_API_KEY_AUTH_KIND, OPENAI_PROVIDER } from "./model-config.js";
|
|
4
|
-
import { createRedactedOpenAiProviderConfigProjection, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
|
|
4
|
+
import { createRedactedOpenAiProviderConfigProjection, withProjectOpenAiProviderConfigLock, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
|
|
5
5
|
function trimOptional(value) {
|
|
6
6
|
const trimmed = value?.trim();
|
|
7
7
|
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
@@ -32,7 +32,7 @@ function validationInput(options) {
|
|
|
32
32
|
}
|
|
33
33
|
return input;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
async function configureProjectOpenAiProviderUnlocked(options) {
|
|
36
36
|
const apiKey = options.apiKey.trim();
|
|
37
37
|
if (!isOpenAiApiKey(apiKey)) {
|
|
38
38
|
return {
|
|
@@ -85,4 +85,7 @@ export async function configureProjectOpenAiProvider(options) {
|
|
|
85
85
|
projection: createRedactedOpenAiProviderConfigProjection(config),
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
export async function configureProjectOpenAiProvider(options) {
|
|
89
|
+
return await withProjectOpenAiProviderConfigLock(options, () => configureProjectOpenAiProviderUnlocked(options));
|
|
90
|
+
}
|
|
88
91
|
//# sourceMappingURL=provider-setup.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
|
-
import type
|
|
2
|
+
import { type ExecutionStatusProjection } from "@tutti/shared/schemas/api";
|
|
3
3
|
import { type HostProjectStore } from "../../store/index.js";
|
|
4
4
|
import type { createHostServer } from "../http/create-server.js";
|
|
5
5
|
import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename, join } from "node:path";
|
|
2
|
+
import { MAX_PROVIDER_MODEL_NAME_LENGTH, } from "@tutti/shared/schemas/api";
|
|
2
3
|
import { serializeRuntimeLogLine } from "@tutti/shared/utils";
|
|
3
4
|
import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
|
|
4
5
|
import { ApprovalRequestManager } from "../../approvals/index.js";
|
|
@@ -11,7 +12,7 @@ import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane
|
|
|
11
12
|
import { cleanupStaleCodexAppServerTempRoots } from "../../providers/openai/app-server/stale-temp-cleanup.js";
|
|
12
13
|
import { CodexAppServerInvocationCoordinator } from "../../providers/openai/app-server/invocation-coordinator.js";
|
|
13
14
|
import { resolveProjectOpenAiReadOnlyChatAssistantModel } from "../../providers/openai/chat-assistant.js";
|
|
14
|
-
import { configureProjectOpenAiProvider, readOpenAiProviderConfigProjection, } from "../../providers/openai/index.js";
|
|
15
|
+
import { configureProjectOpenAiProvider, discoverProjectOpenAiModels, readOpenAiProviderConfigProjection, updateProjectOpenAiDefaultModel, } from "../../providers/openai/index.js";
|
|
15
16
|
import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../provider-usage/index.js";
|
|
16
17
|
import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
|
|
17
18
|
import { openHostProjectStore } from "../../store/index.js";
|
|
@@ -36,6 +37,62 @@ export function packageUpdateReadinessFromExecutionStatus(execution) {
|
|
|
36
37
|
? "busy"
|
|
37
38
|
: "ready";
|
|
38
39
|
}
|
|
40
|
+
function createHostProviderModelService(options) {
|
|
41
|
+
return {
|
|
42
|
+
discover: async () => {
|
|
43
|
+
const result = await discoverProjectOpenAiModels(options);
|
|
44
|
+
if (result.kind !== "ok") {
|
|
45
|
+
return { status: "unavailable", reason: result.reason };
|
|
46
|
+
}
|
|
47
|
+
const models = result.models.filter((model) => model.length <= MAX_PROVIDER_MODEL_NAME_LENGTH && /^\S(?:.*\S)?$/.test(model));
|
|
48
|
+
return models.length > 0
|
|
49
|
+
? { status: "available", models }
|
|
50
|
+
: { status: "unavailable", reason: "model_list_empty" };
|
|
51
|
+
},
|
|
52
|
+
update: async (payload) => {
|
|
53
|
+
const result = await updateProjectOpenAiDefaultModel({
|
|
54
|
+
...options,
|
|
55
|
+
defaultModel: payload.default_model,
|
|
56
|
+
expectedDefaultModel: payload.expected_default_model,
|
|
57
|
+
});
|
|
58
|
+
if (result.kind === "updated") {
|
|
59
|
+
return {
|
|
60
|
+
disposition: { kind: "updated" },
|
|
61
|
+
provider_config: result.projection,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (result.kind === "unchanged") {
|
|
65
|
+
return {
|
|
66
|
+
disposition: { kind: "unchanged" },
|
|
67
|
+
provider_config: result.projection,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (result.kind === "stale_default_model") {
|
|
71
|
+
return {
|
|
72
|
+
disposition: {
|
|
73
|
+
kind: result.kind,
|
|
74
|
+
current_default_model: result.current_default_model,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (result.kind === "validation_failed") {
|
|
79
|
+
return {
|
|
80
|
+
disposition: {
|
|
81
|
+
kind: result.kind,
|
|
82
|
+
reason: result.reason,
|
|
83
|
+
retryable: result.retryable,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
disposition: {
|
|
89
|
+
kind: result.kind,
|
|
90
|
+
reason: result.reason,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
39
96
|
function createHostRuntimeLogger(options) {
|
|
40
97
|
return {
|
|
41
98
|
info(fields, message) {
|
|
@@ -442,6 +499,11 @@ export async function startForegroundHostServer(options) {
|
|
|
442
499
|
chatAssistant,
|
|
443
500
|
tuttiHome: options.preparation.tutti_home,
|
|
444
501
|
getProviderConfig,
|
|
502
|
+
providerModel: createHostProviderModelService({
|
|
503
|
+
tuttiHome: options.preparation.tutti_home,
|
|
504
|
+
projectId: options.preparation.project_id,
|
|
505
|
+
now,
|
|
506
|
+
}),
|
|
445
507
|
getProviderUsage: () => readProviderUsageProjection(store.db),
|
|
446
508
|
updateProjectDisplayName: (displayName) => {
|
|
447
509
|
updateRuntimeProjectDisplayName(displayName);
|
|
@@ -7,5 +7,6 @@ export declare function clarificationRoundInvalidates(roundId: ClarificationRoun
|
|
|
7
7
|
export declare function approvalInvalidates(): QueryInvalidationHint[];
|
|
8
8
|
export declare function readStateInvalidates(): QueryInvalidationHint[];
|
|
9
9
|
export declare function projectMetadataInvalidates(): QueryInvalidationHint[];
|
|
10
|
+
export declare function providerConfigInvalidates(): QueryInvalidationHint[];
|
|
10
11
|
export declare function projectDisplayNameInvalidates(): QueryInvalidationHint[];
|
|
11
12
|
//# sourceMappingURL=invalidations.d.ts.map
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export function mainChatMessageInvalidates() {
|
|
2
|
-
return [
|
|
3
|
-
{ kind: "messages", scope: "main_chat" },
|
|
4
|
-
{ kind: "read_state" },
|
|
5
|
-
{ kind: "bootstrap" },
|
|
6
|
-
];
|
|
2
|
+
return [{ kind: "messages", scope: "main_chat" }, { kind: "read_state" }, { kind: "bootstrap" }];
|
|
7
3
|
}
|
|
8
4
|
export function worklistSubmitInvalidates() {
|
|
9
5
|
return [
|
|
@@ -54,6 +50,9 @@ export function readStateInvalidates() {
|
|
|
54
50
|
export function projectMetadataInvalidates() {
|
|
55
51
|
return [{ kind: "workspace" }, { kind: "bootstrap" }];
|
|
56
52
|
}
|
|
53
|
+
export function providerConfigInvalidates() {
|
|
54
|
+
return [{ kind: "provider_config" }, { kind: "workspace" }, { kind: "bootstrap" }];
|
|
55
|
+
}
|
|
57
56
|
export function projectDisplayNameInvalidates() {
|
|
58
57
|
return projectMetadataInvalidates();
|
|
59
58
|
}
|
|
@@ -76,6 +76,36 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
requestBody?: never;
|
|
79
|
+
} | {
|
|
80
|
+
method: "get";
|
|
81
|
+
path: string;
|
|
82
|
+
summary: string;
|
|
83
|
+
tags: string[];
|
|
84
|
+
responses: {
|
|
85
|
+
200: {
|
|
86
|
+
description: string;
|
|
87
|
+
schema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
88
|
+
status: import("@sinclair/typebox").TLiteral<"available">;
|
|
89
|
+
models: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
90
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
91
|
+
status: import("@sinclair/typebox").TLiteral<"unavailable">;
|
|
92
|
+
reason: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"model_list_unavailable">, import("@sinclair/typebox").TLiteral<"model_list_empty">, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"provider_not_configured">, import("@sinclair/typebox").TLiteral<"provider_config_invalid">, import("@sinclair/typebox").TLiteral<"provider_credential_missing">]>]>;
|
|
93
|
+
}>]>;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
requestBody?: never;
|
|
97
|
+
} | {
|
|
98
|
+
method: "post";
|
|
99
|
+
path: string;
|
|
100
|
+
summary: string;
|
|
101
|
+
tags: string[];
|
|
102
|
+
requestBody: import("@sinclair/typebox").TSchema;
|
|
103
|
+
responses: {
|
|
104
|
+
200: {
|
|
105
|
+
description: string;
|
|
106
|
+
schema: import("@sinclair/typebox").TSchema;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
79
109
|
} | {
|
|
80
110
|
method: "get";
|
|
81
111
|
path: string;
|
|
@@ -555,7 +585,7 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
555
585
|
};
|
|
556
586
|
readonly capabilities: {
|
|
557
587
|
readonly type: "object";
|
|
558
|
-
readonly required: readonly ["sse", "relay_join", "repo_viewer", "provider_config_query", "browser_secret_write", "task_auto_scheduler", "task_run_now_scheduler", "clarification"];
|
|
588
|
+
readonly required: readonly ["sse", "relay_join", "repo_viewer", "provider_config_query", "provider_model_update", "browser_secret_write", "task_auto_scheduler", "task_run_now_scheduler", "clarification"];
|
|
559
589
|
readonly additionalProperties: false;
|
|
560
590
|
readonly properties: {
|
|
561
591
|
readonly sse: {
|
|
@@ -570,6 +600,9 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
570
600
|
readonly provider_config_query: {
|
|
571
601
|
readonly type: "boolean";
|
|
572
602
|
};
|
|
603
|
+
readonly provider_model_update: {
|
|
604
|
+
readonly type: "boolean";
|
|
605
|
+
};
|
|
573
606
|
readonly browser_secret_write: {
|
|
574
607
|
readonly type: "boolean";
|
|
575
608
|
readonly enum: readonly [false];
|
|
@@ -590,18 +623,6 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
590
623
|
};
|
|
591
624
|
};
|
|
592
625
|
requestBody?: never;
|
|
593
|
-
} | {
|
|
594
|
-
method: "post";
|
|
595
|
-
path: string;
|
|
596
|
-
summary: string;
|
|
597
|
-
tags: string[];
|
|
598
|
-
requestBody: import("@sinclair/typebox").TSchema;
|
|
599
|
-
responses: {
|
|
600
|
-
200: {
|
|
601
|
-
description: string;
|
|
602
|
-
schema: import("@sinclair/typebox").TSchema;
|
|
603
|
-
};
|
|
604
|
-
};
|
|
605
626
|
} | {
|
|
606
627
|
method: "get";
|
|
607
628
|
path: string;
|
|
@@ -755,7 +776,7 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
755
776
|
};
|
|
756
777
|
readonly capabilities: {
|
|
757
778
|
readonly type: "object";
|
|
758
|
-
readonly required: readonly ["sse", "relay_join", "repo_viewer", "provider_config_query", "browser_secret_write", "task_auto_scheduler", "task_run_now_scheduler", "clarification"];
|
|
779
|
+
readonly required: readonly ["sse", "relay_join", "repo_viewer", "provider_config_query", "provider_model_update", "browser_secret_write", "task_auto_scheduler", "task_run_now_scheduler", "clarification"];
|
|
759
780
|
readonly additionalProperties: false;
|
|
760
781
|
readonly properties: {
|
|
761
782
|
readonly sse: {
|
|
@@ -770,6 +791,9 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
|
|
|
770
791
|
readonly provider_config_query: {
|
|
771
792
|
readonly type: "boolean";
|
|
772
793
|
};
|
|
794
|
+
readonly provider_model_update: {
|
|
795
|
+
readonly type: "boolean";
|
|
796
|
+
};
|
|
773
797
|
readonly browser_secret_write: {
|
|
774
798
|
readonly type: "boolean";
|
|
775
799
|
readonly enum: readonly [false];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarkReadBodySchema, MarkReadResponseSchema, ReadStateSummaryProjectionSchema, UpdateProjectDescriptionBodySchema, UpdateProjectDescriptionResponseSchema, UpdateProjectDisplayNameBodySchema, UpdateProjectDisplayNameResponseSchema, } from "@tutti/shared/schemas/api";
|
|
1
|
+
import { MarkReadBodySchema, MarkReadResponseSchema, ReadStateSummaryProjectionSchema, UpdateProjectDescriptionBodySchema, UpdateProjectDescriptionResponseSchema, UpdateProjectDisplayNameBodySchema, UpdateProjectDisplayNameResponseSchema, GetProviderModelsResponseSchema, UpdateProviderDefaultModelBodySchema, UpdateProviderDefaultModelResponseSchema, } from "@tutti/shared/schemas/api";
|
|
2
2
|
import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
|
|
3
3
|
import { BootstrapResponseSchema, ProviderConfigProjectionSchema, ProviderUsageProjectionSchema, SessionResponseSchema, WorkspaceResponseSchema, } from "./schemas.js";
|
|
4
4
|
export const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES = [
|
|
@@ -14,6 +14,31 @@ export const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES = [
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
method: "get",
|
|
19
|
+
path: `${HOST_PROJECT_API_BASE_PATH}/provider/models`,
|
|
20
|
+
summary: "Discover model names with the configured provider credential (owner only).",
|
|
21
|
+
tags: ["provider"],
|
|
22
|
+
responses: {
|
|
23
|
+
200: {
|
|
24
|
+
description: "Bounded provider model discovery result.",
|
|
25
|
+
schema: GetProviderModelsResponseSchema,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
method: "post",
|
|
31
|
+
path: `${HOST_PROJECT_API_BASE_PATH}/provider/default-model`,
|
|
32
|
+
summary: "Validate and update the default provider model (owner only).",
|
|
33
|
+
tags: ["provider"],
|
|
34
|
+
requestBody: UpdateProviderDefaultModelBodySchema,
|
|
35
|
+
responses: {
|
|
36
|
+
200: {
|
|
37
|
+
description: "Default-model update command response.",
|
|
38
|
+
schema: UpdateProviderDefaultModelResponseSchema,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
17
42
|
{
|
|
18
43
|
method: "get",
|
|
19
44
|
path: `${HOST_PROJECT_API_BASE_PATH}/provider/usage`,
|