Use the fast model for the onboarding setup chat (#23586)

The workspace setup chat ran on the smart model. The hidden kickoff turn
enqueued its job without a `modelId`, and the frontend sends none unless
the user picks one, so every turn fell through to `modelId ??
workspace.smartModel` in `chat-execution.service.ts`.

Two halves, since the kickoff is server-initiated and the frontend never
sends it:
- `startHiddenKickoffStream` takes a `modelId` and the setup chat passes
`workspace.fastModel`.
- `useAgentChatModelId` requests `workspace.fastModel` on the setup
page, so user turns follow. Everywhere else it still sends nothing and
the server fallback is unchanged.

`workspace.fastModel` defaults to the `default-fast-model` sentinel, so
the model still resolves through the registry and stays
admin-overridable. An explicit pick from the model picker still wins.
This commit is contained in:
Raphaël Bosi
2026-07-31 10:21:02 +02:00
committed by GitHub
parent 8abc8f4bc9
commit a9084604b4
10 changed files with 214 additions and 13 deletions
@@ -85,6 +85,7 @@ describe('AgentChatStreamingService.startHiddenKickoffStream', () => {
userWorkspaceId: 'user-workspace-id',
workspace,
text: kickoffText,
modelId: 'default-fast-model',
};
it('should return null without queueing a visible copy when the claim is lost', async () => {
@@ -124,7 +125,7 @@ describe('AgentChatStreamingService.startHiddenKickoffStream', () => {
});
});
it('should enqueue the hidden kickoff turn without a pinned model and without notifying thread activity', async () => {
it('should enqueue the hidden kickoff turn with the given model and without notifying thread activity', async () => {
const { service, threadRepository, agentChatService, messageQueueService } =
buildService();
@@ -143,13 +144,13 @@ describe('AgentChatStreamingService.startHiddenKickoffStream', () => {
expect.objectContaining({
threadId: 'thread-id',
browsingContext: null,
modelId: 'default-fast-model',
lastUserMessageText: kickoffText,
lastUserMessageParts: [{ type: 'text', text: kickoffText }],
hasTitle: true,
existingTurnId: 'kickoff-turn-id',
}),
);
expect(messageQueueService.add.mock.calls[0][1].modelId).toBeUndefined();
expect(agentChatService.notifyThreadActivityUpdated).not.toHaveBeenCalled();
expect(result).toEqual({
streamId: expect.any(String),
@@ -28,6 +28,8 @@ const EXPECTED_THREAD_ID = v5(
describe('WorkspaceSetupChatService', () => {
const workspace = {
id: 'workspace-id',
fastModel: 'fast-model-id',
smartModel: 'smart-model-id',
} as WorkspaceEntity;
const startArguments = {
@@ -235,6 +237,7 @@ describe('WorkspaceSetupChatService', () => {
text: expect.stringContaining(
'No information about the company that owns this workspace is available.',
),
modelId: 'fast-model-id',
});
expect(result).toEqual({
outcome: WorkspaceSetupChatOutcome.STARTED,
@@ -302,11 +302,13 @@ export class AgentChatStreamingService {
userWorkspaceId,
workspace,
text,
modelId,
}: {
thread: AgentChatThreadEntity;
userWorkspaceId: string;
workspace: WorkspaceEntity;
text: string;
modelId: string;
}): Promise<{ streamId: string; messageId: string; turnId: string } | null> {
const threadId = thread.id;
const streamId = generateId();
@@ -372,6 +374,7 @@ export class AgentChatStreamingService {
workspaceId: workspace.id,
messages,
browsingContext: null,
modelId,
lastUserMessageText: text,
lastUserMessageParts: [{ type: 'text' as const, text }],
hasTitle: !!thread.title,
@@ -389,7 +392,7 @@ export class AgentChatStreamingService {
key: MetricsKeys.AiChatTurnFailed,
amount: 1,
attributes: {
model: 'unknown',
model: modelId,
failure_phase: 'enqueue',
error_code: streamError.code,
},
@@ -164,6 +164,7 @@ export class WorkspaceSetupChatService {
companyEnrichment: companyContext,
locale,
}),
modelId: workspace.fastModel,
});
if (!isDefined(kickoffResult)) {