feat: improve AI chat - system prompt, tool output, context window display (#17769)

⚠️ **AI-generated PR — not ready for review** ⚠️

cc @FelixMalfait

---

## Changes

### System prompt improvements
- Explicit skill-before-tools workflow to prevent the model from calling
tools without loading the matching skill first
- Data efficiency guidance (default small limits, use filters)
- Pluralized `load_skill` → `load_skills` for consistency with
`load_tools`

### Token usage reduction
- Output serialization layer: strips null/undefined/empty values from
tool results
- Lowered default `find_*` limit from 100 → 10, max from 1000 → 100

### System object tool generation
- System objects (calendar events, messages, etc.) now generate AI tools
- Only workflow-related and favorite-related objects are excluded

### Context window display fix
- **Bug**: UI compared cumulative tokens (sum of all turns) against
single-request context window → showed 100% after a few turns
- **Fix**: Track `conversationSize` (last step's `inputTokens`) which
represents the actual conversation history size sent to the model
- New `conversationSize` column on thread entity with migration

### Workspace AI instructions
- Support for custom workspace-level AI instructions

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-09 14:26:02 +01:00
committed by GitHub
parent 6c7c389785
commit 3216b634a3
105 changed files with 3245 additions and 1714 deletions
@@ -4,6 +4,7 @@ export enum ModelProvider {
ANTHROPIC = 'anthropic',
OPENAI_COMPATIBLE = 'open_ai_compatible',
XAI = 'xai',
GROQ = 'groq',
}
export const DEFAULT_FAST_MODEL = 'default-fast-model' as const;
@@ -32,6 +33,8 @@ export type ModelId =
| 'grok-3-mini'
| 'grok-4'
| 'grok-4-1-fast-reasoning'
// Groq models
| 'openai/gpt-oss-120b'
| string; // Allow custom model names
export type SupportedFileType =
@@ -15,6 +15,7 @@ describe('AI_MODELS', () => {
ModelProvider.OPENAI,
ModelProvider.ANTHROPIC,
ModelProvider.XAI,
ModelProvider.GROQ,
];
providers.forEach((provider) => {
@@ -51,6 +52,7 @@ describe('AI_MODELS', () => {
ModelProvider.OPENAI,
ModelProvider.ANTHROPIC,
ModelProvider.XAI,
ModelProvider.GROQ,
];
providers.forEach((provider) => {
@@ -89,7 +91,7 @@ describe('AiModelRegistryService', () => {
MOCK_CONFIG_SERVICE.get.mockReturnValue('gpt-4o');
expect(() => SERVICE.getEffectiveModelConfig(DEFAULT_SMART_MODEL)).toThrow(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY).',
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
);
});
@@ -9,6 +9,7 @@ export {
import { type AIModelConfig } from './ai-models-types.const';
import { ANTHROPIC_MODELS } from './anthropic-models.const';
import { GROQ_MODELS } from './groq-models.const';
import { OPENAI_MODELS } from './openai-models.const';
import { XAI_MODELS } from './xai-models.const';
@@ -16,4 +17,5 @@ export const AI_MODELS: AIModelConfig[] = [
...OPENAI_MODELS,
...ANTHROPIC_MODELS,
...XAI_MODELS,
...GROQ_MODELS,
];
@@ -0,0 +1,15 @@
import { type AIModelConfig, ModelProvider } from './ai-models-types.const';
export const GROQ_MODELS: AIModelConfig[] = [
{
modelId: 'openai/gpt-oss-120b',
label: 'GPT-OSS 120B (Groq)',
description:
'Large-scale open-source model with browser search, served via Groq inference',
provider: ModelProvider.GROQ,
inputCostPer1kTokensInCents: 0.059,
outputCostPer1kTokensInCents: 0.079,
contextWindowTokens: 128000,
maxOutputTokens: 16384,
},
];
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { anthropic } from '@ai-sdk/anthropic';
import { groq } from '@ai-sdk/groq';
import { createOpenAI, openai } from '@ai-sdk/openai';
import { xai } from '@ai-sdk/xai';
import { type LanguageModel } from 'ai';
@@ -18,6 +19,7 @@ import {
type AIModelConfig,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { ANTHROPIC_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/anthropic-models.const';
import { GROQ_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/groq-models.const';
import { OPENAI_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/openai-models.const';
import { XAI_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/xai-models.const';
@@ -57,6 +59,12 @@ export class AiModelRegistryService {
this.registerXaiModels();
}
const groqApiKey = this.twentyConfigService.get('GROQ_API_KEY');
if (groqApiKey) {
this.registerGroqModels();
}
const openaiCompatibleBaseUrl = this.twentyConfigService.get(
'OPENAI_COMPATIBLE_BASE_URL',
);
@@ -105,6 +113,17 @@ export class AiModelRegistryService {
});
}
private registerGroqModels(): void {
GROQ_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
provider: ModelProvider.GROQ,
model: groq(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
});
}
private registerOpenAICompatibleModels(
baseUrl: string,
modelNamesString: string,
@@ -170,7 +189,7 @@ export class AiModelRegistryService {
if (!model) {
throw new AgentException(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY).',
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}
@@ -192,7 +211,7 @@ export class AiModelRegistryService {
if (!model) {
throw new AgentException(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY).',
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}
@@ -290,6 +309,9 @@ export class AiModelRegistryService {
case ModelProvider.XAI:
apiKey = this.twentyConfigService.get('XAI_API_KEY');
break;
case ModelProvider.GROQ:
apiKey = this.twentyConfigService.get('GROQ_API_KEY');
break;
case ModelProvider.OPENAI_COMPATIBLE:
apiKey = this.twentyConfigService.get('OPENAI_COMPATIBLE_API_KEY');
break;