@chat-js/cli 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +216 -171
- package/package.json +1 -1
- package/templates/chat-app/CHANGELOG.md +19 -0
- package/templates/chat-app/app/(chat)/actions.ts +9 -9
- package/templates/chat-app/app/(chat)/api/chat/prepare/route.ts +94 -0
- package/templates/chat-app/app/(chat)/api/chat/route.ts +97 -14
- package/templates/chat-app/chat.config.ts +144 -156
- package/templates/chat-app/components/chat-sync.tsx +6 -3
- package/templates/chat-app/components/feedback-actions.tsx +7 -3
- package/templates/chat-app/components/message-editor.tsx +8 -3
- package/templates/chat-app/components/message-siblings.tsx +14 -1
- package/templates/chat-app/components/model-selector.tsx +669 -407
- package/templates/chat-app/components/multimodal-input.tsx +252 -18
- package/templates/chat-app/components/parallel-response-cards.tsx +157 -0
- package/templates/chat-app/components/part/text-message-part.tsx +9 -5
- package/templates/chat-app/components/retry-button.tsx +25 -8
- package/templates/chat-app/components/user-message.tsx +136 -125
- package/templates/chat-app/hooks/chat-sync-hooks.ts +11 -0
- package/templates/chat-app/hooks/use-navigate-to-message.ts +39 -0
- package/templates/chat-app/lib/ai/gateway-model-defaults.ts +154 -100
- package/templates/chat-app/lib/ai/gateways/openrouter-gateway.ts +2 -2
- package/templates/chat-app/lib/ai/tools/generate-image.ts +9 -2
- package/templates/chat-app/lib/ai/tools/generate-video.ts +3 -0
- package/templates/chat-app/lib/ai/types.ts +74 -3
- package/templates/chat-app/lib/config-schema.ts +131 -132
- package/templates/chat-app/lib/config.ts +2 -2
- package/templates/chat-app/lib/db/migrations/0044_gray_red_shift.sql +5 -0
- package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +1567 -0
- package/templates/chat-app/lib/db/migrations/meta/_journal.json +8 -1
- package/templates/chat-app/lib/db/queries.ts +84 -4
- package/templates/chat-app/lib/db/schema.ts +4 -1
- package/templates/chat-app/lib/message-conversion.ts +14 -2
- package/templates/chat-app/lib/stores/hooks-threads.ts +37 -1
- package/templates/chat-app/lib/stores/with-threads.test.ts +137 -0
- package/templates/chat-app/lib/stores/with-threads.ts +157 -4
- package/templates/chat-app/lib/thread-utils.ts +23 -2
- package/templates/chat-app/package.json +1 -1
- package/templates/chat-app/providers/chat-input-provider.tsx +40 -2
- package/templates/chat-app/scripts/db-branch-delete.sh +7 -1
- package/templates/chat-app/scripts/db-branch-use.sh +7 -1
- package/templates/chat-app/scripts/with-db.sh +7 -1
- package/templates/chat-app/vitest.config.ts +2 -0
|
@@ -41,6 +41,9 @@ async function resolveImageModel(selectedModel?: string): Promise<{
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Fall back to the configured default image model
|
|
44
|
+
if (!config.ai.tools.image.enabled) {
|
|
45
|
+
throw new Error("Image generation is not enabled");
|
|
46
|
+
}
|
|
44
47
|
const defaultId = config.ai.tools.image.default;
|
|
45
48
|
try {
|
|
46
49
|
const model = await getAppModelDefinition(defaultId as AppModelId);
|
|
@@ -134,6 +137,10 @@ async function runGenerateImageTraditional({
|
|
|
134
137
|
startMs: number;
|
|
135
138
|
costAccumulator?: CostAccumulator;
|
|
136
139
|
}): Promise<{ imageUrl: string; prompt: string }> {
|
|
140
|
+
if (!config.ai.tools.image.enabled) {
|
|
141
|
+
throw new Error("Image generation is not enabled");
|
|
142
|
+
}
|
|
143
|
+
const imageDefault = config.ai.tools.image.default;
|
|
137
144
|
let promptInput:
|
|
138
145
|
| string
|
|
139
146
|
| {
|
|
@@ -161,7 +168,7 @@ async function runGenerateImageTraditional({
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
const res = await generateImage({
|
|
164
|
-
model: getImageModel(
|
|
171
|
+
model: getImageModel(imageDefault),
|
|
165
172
|
prompt: promptInput,
|
|
166
173
|
n: 1,
|
|
167
174
|
providerOptions: {
|
|
@@ -184,7 +191,7 @@ async function runGenerateImageTraditional({
|
|
|
184
191
|
|
|
185
192
|
if (res.usage) {
|
|
186
193
|
costAccumulator?.addLLMCost(
|
|
187
|
-
|
|
194
|
+
imageDefault as AppModelId,
|
|
188
195
|
{
|
|
189
196
|
inputTokens: res.usage.inputTokens,
|
|
190
197
|
outputTokens: res.usage.outputTokens,
|
|
@@ -48,6 +48,9 @@ async function resolveVideoModel(selectedModel?: string): Promise<string> {
|
|
|
48
48
|
// Not in app models registry, fall through
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
if (!config.ai.tools.video.enabled) {
|
|
52
|
+
throw new Error("Video generation is not enabled");
|
|
53
|
+
}
|
|
51
54
|
return config.ai.tools.video.default;
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -59,10 +59,83 @@ const frontendToolsSchema = z.enum([
|
|
|
59
59
|
const __ = frontendToolsSchema.options satisfies ToolNameInternal[];
|
|
60
60
|
|
|
61
61
|
export type UiToolName = z.infer<typeof frontendToolsSchema>;
|
|
62
|
+
|
|
63
|
+
export type SelectedModelCounts = Partial<Record<AppModelId, number>>;
|
|
64
|
+
export type SelectedModelValue = AppModelId | SelectedModelCounts;
|
|
65
|
+
|
|
66
|
+
export function isSelectedModelCounts(
|
|
67
|
+
value: unknown
|
|
68
|
+
): value is SelectedModelCounts {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Object.keys(value).length === 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Object.entries(value).every(
|
|
78
|
+
([modelId, count]) =>
|
|
79
|
+
typeof modelId === "string" &&
|
|
80
|
+
typeof count === "number" &&
|
|
81
|
+
Number.isInteger(count) &&
|
|
82
|
+
count > 0
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isSelectedModelValue(
|
|
87
|
+
value: unknown
|
|
88
|
+
): value is SelectedModelValue {
|
|
89
|
+
return typeof value === "string" || isSelectedModelCounts(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getPrimarySelectedModelId(
|
|
93
|
+
selectedModel: SelectedModelValue | null | undefined
|
|
94
|
+
): AppModelId | null {
|
|
95
|
+
if (!selectedModel) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof selectedModel === "string") {
|
|
100
|
+
return selectedModel;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const [firstSelectedModelId] = Object.entries(selectedModel).find(
|
|
104
|
+
([, count]) => typeof count === "number" && count > 0
|
|
105
|
+
) ?? [null];
|
|
106
|
+
|
|
107
|
+
return firstSelectedModelId as AppModelId | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function expandSelectedModelValue(
|
|
111
|
+
selectedModel: SelectedModelValue
|
|
112
|
+
): AppModelId[] {
|
|
113
|
+
if (typeof selectedModel === "string") {
|
|
114
|
+
return [selectedModel];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const expanded: AppModelId[] = [];
|
|
118
|
+
|
|
119
|
+
for (const [modelId, count] of Object.entries(selectedModel)) {
|
|
120
|
+
if (!(typeof count === "number" && Number.isInteger(count) && count > 0)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (let index = 0; index < count; index += 1) {
|
|
125
|
+
expanded.push(modelId as AppModelId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return expanded;
|
|
130
|
+
}
|
|
131
|
+
|
|
62
132
|
const messageMetadataSchema = z.object({
|
|
63
133
|
createdAt: z.date(),
|
|
64
134
|
parentMessageId: z.string().nullable(),
|
|
65
|
-
|
|
135
|
+
parallelGroupId: z.string().nullable().optional(),
|
|
136
|
+
parallelIndex: z.number().int().nullable().optional(),
|
|
137
|
+
isPrimaryParallel: z.boolean().nullable().optional(),
|
|
138
|
+
selectedModel: z.custom<SelectedModelValue>(isSelectedModelValue),
|
|
66
139
|
activeStreamId: z.string().nullable(),
|
|
67
140
|
selectedTool: frontendToolsSchema.optional(),
|
|
68
141
|
usage: z.custom<LanguageModelUsage | undefined>((_val) => true).optional(),
|
|
@@ -101,7 +174,6 @@ type webSearchTool = InferUITool<ReturnType<typeof tavilyWebSearch>>;
|
|
|
101
174
|
type codeExecutionTool = InferUITool<ReturnType<typeof codeExecution>>;
|
|
102
175
|
type retrieveUrlTool = InferUITool<typeof retrieveUrl>;
|
|
103
176
|
|
|
104
|
-
// biome-ignore lint/style/useConsistentTypeDefinitions: <explanation>
|
|
105
177
|
export type ChatTools = {
|
|
106
178
|
codeExecution: codeExecutionTool;
|
|
107
179
|
createCodeDocument: createCodeDocumentToolType;
|
|
@@ -123,7 +195,6 @@ interface FollowupSuggestions {
|
|
|
123
195
|
suggestions: string[];
|
|
124
196
|
}
|
|
125
197
|
|
|
126
|
-
// biome-ignore lint/style/useConsistentTypeDefinitions: <explanation>
|
|
127
198
|
export type CustomUIDataTypes = {
|
|
128
199
|
appendMessage: string;
|
|
129
200
|
chatConfirmed: {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
GatewayType,
|
|
6
6
|
GatewayVideoModelIdMap,
|
|
7
7
|
} from "@/lib/ai/gateways/registry";
|
|
8
|
+
import { GATEWAY_MODEL_DEFAULTS } from "./ai/gateway-model-defaults";
|
|
8
9
|
import type { ToolName } from "./ai/types";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_GATEWAY = "vercel" as const satisfies GatewayType;
|
|
@@ -105,14 +106,26 @@ function createAiSchema<G extends GatewayType>(g: G) {
|
|
|
105
106
|
code: z.object({
|
|
106
107
|
edits: gatewayModelId<G>(),
|
|
107
108
|
}),
|
|
108
|
-
image: z.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
image: z.discriminatedUnion("enabled", [
|
|
110
|
+
z.object({
|
|
111
|
+
enabled: z.literal(true),
|
|
112
|
+
default: gatewayImageModelId<G>(),
|
|
113
|
+
}),
|
|
114
|
+
z.object({
|
|
115
|
+
enabled: z.literal(false),
|
|
116
|
+
default: gatewayImageModelId<G>().optional(),
|
|
117
|
+
}),
|
|
118
|
+
]),
|
|
119
|
+
video: z.discriminatedUnion("enabled", [
|
|
120
|
+
z.object({
|
|
121
|
+
enabled: z.literal(true),
|
|
122
|
+
default: gatewayVideoModelId<G>(),
|
|
123
|
+
}),
|
|
124
|
+
z.object({
|
|
125
|
+
enabled: z.literal(false),
|
|
126
|
+
default: gatewayVideoModelId<G>().optional(),
|
|
127
|
+
}),
|
|
128
|
+
]),
|
|
116
129
|
deepResearch: deepResearchToolConfigSchema.extend({
|
|
117
130
|
enabled: z.boolean(),
|
|
118
131
|
defaultModel: gatewayModelId<G>(),
|
|
@@ -142,77 +155,7 @@ export const aiConfigSchema = z
|
|
|
142
155
|
])
|
|
143
156
|
.default({
|
|
144
157
|
gateway: DEFAULT_GATEWAY,
|
|
145
|
-
|
|
146
|
-
disabledModels: [],
|
|
147
|
-
curatedDefaults: [
|
|
148
|
-
// OpenAI
|
|
149
|
-
"openai/gpt-5-nano",
|
|
150
|
-
"openai/gpt-5-mini",
|
|
151
|
-
"openai/gpt-5.2",
|
|
152
|
-
"openai/gpt-5.2-chat",
|
|
153
|
-
|
|
154
|
-
// Google
|
|
155
|
-
"google/gemini-2.5-flash-lite",
|
|
156
|
-
"google/gemini-3-flash",
|
|
157
|
-
"google/gemini-3-pro-preview",
|
|
158
|
-
// Anthropic
|
|
159
|
-
"anthropic/claude-sonnet-4.5",
|
|
160
|
-
"anthropic/claude-opus-4.5",
|
|
161
|
-
// xAI
|
|
162
|
-
"xai/grok-4",
|
|
163
|
-
],
|
|
164
|
-
anonymousModels: ["google/gemini-2.5-flash-lite", "openai/gpt-5-nano"],
|
|
165
|
-
workflows: {
|
|
166
|
-
chat: "openai/gpt-5-mini",
|
|
167
|
-
title: "openai/gpt-5-nano",
|
|
168
|
-
pdf: "openai/gpt-5-mini",
|
|
169
|
-
chatImageCompatible: "openai/gpt-4o-mini",
|
|
170
|
-
},
|
|
171
|
-
tools: {
|
|
172
|
-
webSearch: {
|
|
173
|
-
enabled: false,
|
|
174
|
-
},
|
|
175
|
-
urlRetrieval: {
|
|
176
|
-
enabled: false,
|
|
177
|
-
},
|
|
178
|
-
codeExecution: {
|
|
179
|
-
enabled: false,
|
|
180
|
-
},
|
|
181
|
-
mcp: {
|
|
182
|
-
enabled: false,
|
|
183
|
-
},
|
|
184
|
-
followupSuggestions: {
|
|
185
|
-
enabled: false,
|
|
186
|
-
default: "google/gemini-2.5-flash-lite",
|
|
187
|
-
},
|
|
188
|
-
text: {
|
|
189
|
-
polish: "openai/gpt-5-mini",
|
|
190
|
-
},
|
|
191
|
-
sheet: {
|
|
192
|
-
format: "openai/gpt-5-mini",
|
|
193
|
-
analyze: "openai/gpt-5-mini",
|
|
194
|
-
},
|
|
195
|
-
code: {
|
|
196
|
-
edits: "openai/gpt-5-mini",
|
|
197
|
-
},
|
|
198
|
-
image: {
|
|
199
|
-
enabled: false,
|
|
200
|
-
default: "google/gemini-3-pro-image",
|
|
201
|
-
},
|
|
202
|
-
video: {
|
|
203
|
-
enabled: false,
|
|
204
|
-
default: "xai/grok-imagine-video",
|
|
205
|
-
},
|
|
206
|
-
deepResearch: {
|
|
207
|
-
enabled: false,
|
|
208
|
-
defaultModel: "google/gemini-2.5-flash-lite",
|
|
209
|
-
finalReportModel: "google/gemini-3-flash",
|
|
210
|
-
allowClarification: true,
|
|
211
|
-
maxResearcherIterations: 1,
|
|
212
|
-
maxConcurrentResearchUnits: 2,
|
|
213
|
-
maxSearchQueries: 2,
|
|
214
|
-
},
|
|
215
|
-
},
|
|
158
|
+
...GATEWAY_MODEL_DEFAULTS[DEFAULT_GATEWAY],
|
|
216
159
|
});
|
|
217
160
|
|
|
218
161
|
export const pricingConfigSchema = z.object({
|
|
@@ -281,9 +224,14 @@ export const featuresConfigSchema = z
|
|
|
281
224
|
attachments: z
|
|
282
225
|
.boolean()
|
|
283
226
|
.describe("File attachments (requires BLOB_READ_WRITE_TOKEN)"),
|
|
227
|
+
parallelResponses: z
|
|
228
|
+
.boolean()
|
|
229
|
+
.default(true)
|
|
230
|
+
.describe("Send one message to multiple models simultaneously"),
|
|
284
231
|
})
|
|
285
232
|
.default({
|
|
286
233
|
attachments: false,
|
|
234
|
+
parallelResponses: true,
|
|
287
235
|
});
|
|
288
236
|
|
|
289
237
|
export const authenticationConfigSchema = z
|
|
@@ -400,63 +348,58 @@ type ZodConfigInput = z.input<typeof configSchema>;
|
|
|
400
348
|
// Use vercel variant as shape reference (all variants share the same structure)
|
|
401
349
|
type AiShape = z.input<typeof gatewaySchemaMap.vercel>;
|
|
402
350
|
type AiToolsShape = AiShape["tools"];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
> & {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
>
|
|
351
|
+
|
|
352
|
+
// All helper types are Partial — fields not provided are filled by applyDefaults
|
|
353
|
+
type DeepResearchToolInputFor<G extends GatewayType> = Partial<
|
|
354
|
+
Omit<AiToolsShape["deepResearch"], "defaultModel" | "finalReportModel"> & {
|
|
355
|
+
defaultModel: GatewayModelIdMap[G];
|
|
356
|
+
finalReportModel: GatewayModelIdMap[G];
|
|
357
|
+
}
|
|
358
|
+
>;
|
|
359
|
+
type ImageToolInputFor<G extends GatewayType> = [
|
|
360
|
+
GatewayImageModelIdMap[G],
|
|
361
|
+
] extends [never]
|
|
362
|
+
? { enabled?: false }
|
|
363
|
+
:
|
|
364
|
+
| { enabled: true; default: GatewayImageModelIdMap[G] }
|
|
365
|
+
| { enabled?: false; default?: GatewayImageModelIdMap[G] };
|
|
366
|
+
type VideoToolInputFor<G extends GatewayType> = [
|
|
367
|
+
GatewayVideoModelIdMap[G],
|
|
368
|
+
] extends [never]
|
|
369
|
+
? { enabled?: false }
|
|
370
|
+
:
|
|
371
|
+
| { enabled: true; default: GatewayVideoModelIdMap[G] }
|
|
372
|
+
| { enabled?: false; default?: GatewayVideoModelIdMap[G] };
|
|
373
|
+
type FollowupSuggestionsToolInputFor<G extends GatewayType> = Partial<{
|
|
374
|
+
enabled: boolean;
|
|
426
375
|
default: GatewayModelIdMap[G];
|
|
427
|
-
}
|
|
376
|
+
}>;
|
|
428
377
|
interface AiToolsInputFor<G extends GatewayType> {
|
|
429
|
-
code:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
text: {
|
|
441
|
-
[P in keyof AiToolsShape["text"]]: GatewayModelIdMap[G];
|
|
442
|
-
};
|
|
443
|
-
urlRetrieval: AiToolsShape["urlRetrieval"];
|
|
444
|
-
video: VideoToolInputFor<G>;
|
|
445
|
-
webSearch: AiToolsShape["webSearch"];
|
|
378
|
+
code?: Partial<{ [P in keyof AiToolsShape["code"]]: GatewayModelIdMap[G] }>;
|
|
379
|
+
codeExecution?: Partial<AiToolsShape["codeExecution"]>;
|
|
380
|
+
deepResearch?: DeepResearchToolInputFor<G>;
|
|
381
|
+
followupSuggestions?: FollowupSuggestionsToolInputFor<G>;
|
|
382
|
+
image?: ImageToolInputFor<G>;
|
|
383
|
+
mcp?: Partial<AiToolsShape["mcp"]>;
|
|
384
|
+
sheet?: Partial<{ [P in keyof AiToolsShape["sheet"]]: GatewayModelIdMap[G] }>;
|
|
385
|
+
text?: Partial<{ [P in keyof AiToolsShape["text"]]: GatewayModelIdMap[G] }>;
|
|
386
|
+
urlRetrieval?: Partial<AiToolsShape["urlRetrieval"]>;
|
|
387
|
+
video?: VideoToolInputFor<G>;
|
|
388
|
+
webSearch?: Partial<AiToolsShape["webSearch"]>;
|
|
446
389
|
}
|
|
447
390
|
|
|
391
|
+
// Only gateway is required; everything else is an override on top of GATEWAY_MODEL_DEFAULTS
|
|
392
|
+
// biome-ignore lint/style/useConsistentTypeDefinitions: type is used intentionally here
|
|
448
393
|
type AiInputFor<G extends GatewayType> = {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
? GatewayModelIdMap[G][]
|
|
459
|
-
: AiShape[K];
|
|
394
|
+
gateway: G;
|
|
395
|
+
providerOrder?: AiShape["providerOrder"];
|
|
396
|
+
disabledModels?: GatewayModelIdMap[G][];
|
|
397
|
+
curatedDefaults?: GatewayModelIdMap[G][];
|
|
398
|
+
anonymousModels?: GatewayModelIdMap[G][];
|
|
399
|
+
workflows?: Partial<{
|
|
400
|
+
[W in keyof AiShape["workflows"]]: GatewayModelIdMap[G];
|
|
401
|
+
}>;
|
|
402
|
+
tools?: AiToolsInputFor<G>;
|
|
460
403
|
};
|
|
461
404
|
|
|
462
405
|
type ConfigInputForGateway<G extends GatewayType> = Omit<
|
|
@@ -470,7 +413,63 @@ export type ConfigInput = {
|
|
|
470
413
|
[G in GatewayType]: ConfigInputForGateway<G>;
|
|
471
414
|
}[GatewayType];
|
|
472
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Type-safe config helper. Infers the gateway type from `ai.gateway` so
|
|
418
|
+
* autocomplete and error messages are scoped to the chosen gateway's model IDs.
|
|
419
|
+
* Only `ai.gateway` is required — all other `ai` fields are optional overrides
|
|
420
|
+
* on top of the gateway defaults supplied by `applyDefaults`.
|
|
421
|
+
*/
|
|
422
|
+
export function defineConfig<G extends GatewayType>(
|
|
423
|
+
config: ConfigInputForGateway<G>
|
|
424
|
+
): ConfigInput {
|
|
425
|
+
return config as ConfigInput;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function mergeToolsConfig<T extends Record<string, unknown>>(
|
|
429
|
+
defaults: T,
|
|
430
|
+
user: Record<string, unknown> | undefined
|
|
431
|
+
): T {
|
|
432
|
+
if (!user) {
|
|
433
|
+
return defaults;
|
|
434
|
+
}
|
|
435
|
+
const result: Record<string, unknown> = { ...defaults };
|
|
436
|
+
for (const [key, val] of Object.entries(user)) {
|
|
437
|
+
const defVal = result[key];
|
|
438
|
+
if (
|
|
439
|
+
val !== null &&
|
|
440
|
+
typeof val === "object" &&
|
|
441
|
+
!Array.isArray(val) &&
|
|
442
|
+
defVal !== null &&
|
|
443
|
+
typeof defVal === "object" &&
|
|
444
|
+
!Array.isArray(defVal)
|
|
445
|
+
) {
|
|
446
|
+
result[key] = { ...defVal, ...(val as object) };
|
|
447
|
+
} else {
|
|
448
|
+
result[key] = val;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return result as T;
|
|
452
|
+
}
|
|
453
|
+
|
|
473
454
|
// Apply defaults to partial config
|
|
474
455
|
export function applyDefaults(input: ConfigInput): Config {
|
|
475
|
-
|
|
456
|
+
const gateway = input.ai?.gateway ?? DEFAULT_GATEWAY;
|
|
457
|
+
const gatewayDefaults = GATEWAY_MODEL_DEFAULTS[gateway];
|
|
458
|
+
const aiInput = input.ai as Record<string, unknown> | undefined;
|
|
459
|
+
|
|
460
|
+
const mergedAi = {
|
|
461
|
+
gateway,
|
|
462
|
+
...gatewayDefaults,
|
|
463
|
+
...aiInput,
|
|
464
|
+
workflows: {
|
|
465
|
+
...gatewayDefaults.workflows,
|
|
466
|
+
...(aiInput?.workflows as Record<string, unknown> | undefined),
|
|
467
|
+
},
|
|
468
|
+
tools: mergeToolsConfig(
|
|
469
|
+
gatewayDefaults.tools,
|
|
470
|
+
aiInput?.tools as Record<string, unknown> | undefined
|
|
471
|
+
),
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return configSchema.parse({ ...input, ai: mergedAi });
|
|
476
475
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import userConfig from "@/chat.config";
|
|
2
2
|
import type { ActiveGatewayType } from "./ai/app-model-id";
|
|
3
|
-
import { type AiConfig, type Config
|
|
3
|
+
import { type AiConfig, applyDefaults, type Config } from "./config-schema";
|
|
4
4
|
|
|
5
5
|
type ActiveAiConfig = Extract<AiConfig, { gateway: ActiveGatewayType }>;
|
|
6
6
|
|
|
@@ -15,6 +15,6 @@ type ActiveConfig = Omit<Config, "ai"> & { ai: ActiveAiConfig };
|
|
|
15
15
|
* import { config } from "@/lib/config";
|
|
16
16
|
* console.log(config.appName);
|
|
17
17
|
*/
|
|
18
|
-
export const config =
|
|
18
|
+
export const config = applyDefaults(userConfig) as ActiveConfig;
|
|
19
19
|
|
|
20
20
|
export type { Config } from "./config-schema";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
ALTER TABLE "Message" ALTER COLUMN "selectedModel" DROP DEFAULT;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "Message" ALTER COLUMN "selectedModel" SET DATA TYPE json USING to_json("selectedModel");--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "Message" ADD COLUMN "parallelGroupId" uuid;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "Message" ADD COLUMN "parallelIndex" integer;--> statement-breakpoint
|
|
5
|
+
ALTER TABLE "Message" ADD COLUMN "isPrimaryParallel" boolean;--> statement-breakpoint
|