@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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/openai/index.d.ts +1 -0
  3. package/dist/providers/openai/index.js +1 -0
  4. package/dist/providers/openai/machine-local-files.d.ts +1 -1
  5. package/dist/providers/openai/machine-local-files.js +32 -9
  6. package/dist/providers/openai/provider-config.d.ts +5 -1
  7. package/dist/providers/openai/provider-config.js +25 -4
  8. package/dist/providers/openai/provider-models.d.ts +41 -0
  9. package/dist/providers/openai/provider-models.js +82 -0
  10. package/dist/providers/openai/provider-setup.js +5 -2
  11. package/dist/server-shell/cli/host-server-runtime.d.ts +1 -1
  12. package/dist/server-shell/cli/host-server-runtime.js +63 -1
  13. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +1 -0
  14. package/dist/server-shell/http/routes/project-api/invalidations.js +4 -5
  15. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +38 -14
  16. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.js +26 -1
  17. package/dist/server-shell/http/routes/project-api/openapi.d.ts +38 -14
  18. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -2
  19. package/dist/server-shell/http/routes/project-api/schemas.js +2 -0
  20. package/dist/server-shell/http/routes/project-api/session-requirements.d.ts +1 -0
  21. package/dist/server-shell/http/routes/project-api/session-requirements.js +7 -0
  22. package/dist/server-shell/http/routes/project-api/system-routes.js +96 -5
  23. package/dist/server-shell/http/routes/project-api/types.d.ts +10 -1
  24. package/dist/server-shell/http/routes/project-api/workspace-projections.js +4 -2
  25. package/dist/server-shell/http/routes/project-api.d.ts +1 -1
  26. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  27. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  28. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +36 -0
  29. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +58 -1
  30. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +9 -1
  31. package/package.json +1 -1
  32. package/web/assets/{homepage-motion-scene-BFQnHtRi.js → homepage-motion-scene-DXzmjALa.js} +1 -1
  33. package/web/assets/{index-C4cdSaSb.js → index-9gApbtPC.js} +3 -3
  34. package/web/assets/index-BKeSEhsn.css +1 -0
  35. package/web/index.html +2 -2
  36. 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 可视化产物查看层和 OpenAI / Codex provider 接入。
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 writePrivateTextFile(filePath: string, value: string): void;
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 { dirname } from "node:path";
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 writePrivateTextFile(filePath, value) {
19
- ensurePrivateDirectory(dirname(filePath));
20
- writeFileSync(filePath, value, {
21
- encoding: "utf8",
22
- mode: 0o600,
23
- });
24
- chmodSync(filePath, 0o600);
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, } from "./provider-config-shape.js";
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, } from "./credential-store.js";
4
- import { readTextFile, writePrivateTextFile } from "./machine-local-files.js";
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, } from "./provider-config-shape.js";
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
- writePrivateTextFile(getProjectProviderConfigPath(tuttiHome, projectId), formatProjectOpenAiProviderConfig(config));
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
- export async function configureProjectOpenAiProvider(options) {
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 { ExecutionStatusProjection } from "@tutti/shared/schemas/api";
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`,