fix(ai): route xAI search through Responses API as native tools (#21037)

xAI deprecated Live Search, so the `searchParameters` provider option
now returns 410. This routes all xAI models through the Responses API
and binds web/X search as native agent tools, matching how
Anthropic/OpenAI expose search.

- xAI provider now uses `provider.responses()` — its
`webSearch()`/`xSearch()` tools only run against the Responses endpoint,
not chat completions
- web/X search migrated from the `provider-option` variant to `sdk-tool`
(`web_search`/`x_search`); deleted the dead `searchParameters` path, the
`provider-option` variant, and `providerOptions` on `NativeModelBinding`
- dropped a dead `rolePermissionConfig` param on `getAgentRoleId`, left
over from #20331

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
This commit is contained in:
nitin
2026-05-31 18:41:04 +05:30
committed by GitHub
parent b027e4bdb1
commit e430e4ea0a
11 changed files with 252 additions and 226 deletions
@@ -79,7 +79,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti
{
provide: NativeToolBinderService,
useValue: {
bind: jest.fn().mockReturnValue({ tools: {}, providerOptions: {} }),
bind: jest.fn().mockReturnValue({}),
},
},
{
@@ -18,7 +18,6 @@ import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-a
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
import { NativeToolBinderService } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.service';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -39,6 +38,7 @@ import { mergeLanguageModelUsage } from 'src/engine/metadata-modules/ai/ai-billi
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-config.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { NativeToolBinderService } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.service';
import { type NativeModelToolOptions } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-tool-options.type';
import {
AiException,
@@ -87,7 +87,6 @@ export class AgentAsyncExecutorService {
private async getAgentRoleId(
agentId: string,
workspaceId: string,
rolePermissionConfig?: RolePermissionConfig,
): Promise<string | undefined> {
const roleTarget = await this.roleTargetRepository.findOne(workspaceId, {
where: {
@@ -188,22 +187,20 @@ export class AgentAsyncExecutorService {
);
}
const nativeBinding = this.nativeToolBinder.bind(
const nativeTools = this.nativeToolBinder.bind(
registeredModel,
nativeModelToolOptions,
);
tools = {
...registryTools,
...nativeBinding.tools,
...nativeTools,
};
providerOptions = {
...nativeBinding.providerOptions,
...this.aiModelConfigService.getReasoningProviderOptions(
providerOptions =
this.aiModelConfigService.getReasoningProviderOptions(
registeredModel,
),
};
);
}
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
@@ -25,7 +25,6 @@ import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-pr
import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { NativeToolBinderService } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.service';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
createExecuteToolTool,
@@ -61,6 +60,7 @@ import {
} from 'src/engine/metadata-modules/ai/ai-chat/utils/inject-cache-breakpoint.util';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { NativeToolBinderService } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.service';
import { type AiModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-config.type';
import { getNativeModelCapabilities } from 'src/engine/metadata-modules/ai/ai-models/utils/get-native-model-capabilities.util';
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
@@ -168,7 +168,7 @@ export class ChatExecutionService {
const nativeCapabilities = getNativeModelCapabilities(
registeredModel.sdkPackage,
);
const nativeBinding = this.nativeToolBinder.bind(registeredModel, {
const nativeTools = this.nativeToolBinder.bind(registeredModel, {
webSearch: nativeCapabilities?.webSearch === true,
twitterSearch: nativeCapabilities?.twitterSearch === true,
});
@@ -178,12 +178,12 @@ export class ChatExecutionService {
// serialized). execute_tool routes discovered tools through the registry.
const directTools: ToolSet = {
...preloadedTools,
...nativeBinding.tools,
...nativeTools,
};
const preloadedToolNames = [
...Object.keys(preloadedTools),
...Object.keys(nativeBinding.tools),
...Object.keys(nativeTools),
];
// ToolSet is constant for the entire conversation — no mutation.
@@ -399,10 +399,9 @@ export class ChatExecutionService {
stopWhen: (step) =>
stepCountIs(AGENT_CONFIG.MAX_STEPS)(step) || hasNoMoreAvailableCredits,
experimental_telemetry: AI_TELEMETRY_CONFIG,
providerOptions: {
...nativeBinding.providerOptions,
...getCallLevelCacheProviderOptions(registeredModel.sdkPackage),
},
providerOptions: getCallLevelCacheProviderOptions(
registeredModel.sdkPackage,
),
prepareStep: ({ messages }) => {
stepStartedAt = performance.now();
@@ -27,12 +27,12 @@ export const NATIVE_MODEL_TOOLS_BY_SDK_PACKAGE = {
},
[AI_SDK_XAI]: {
webSearch: {
kind: 'provider-option',
providerOptionKey: 'web',
kind: 'sdk-tool',
directToolName: 'web_search',
},
twitterSearch: {
kind: 'provider-option',
providerOptionKey: 'x',
kind: 'sdk-tool',
directToolName: 'x_search',
},
},
[AI_SDK_GOOGLE]: {},
@@ -1,7 +1,9 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import {
AI_SDK_ANTHROPIC,
AI_SDK_OPENAI,
AI_SDK_XAI,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-sdk-package.const';
import { AiModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-config.service';
@@ -11,124 +13,206 @@ import {
} from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-models/services/sdk-provider-factory.service';
describe('AiModelConfigService.getNativeModelBinding — xAI search parameters', () => {
const XAI_WEB_TOOL = { __tool: 'xai-web-search' };
const XAI_X_TOOL = { __tool: 'xai-x-search' };
const ANTHROPIC_WEB_TOOL = { __tool: 'anthropic-web-search' };
const OPENAI_WEB_TOOL = { __tool: 'openai-web-search' };
describe('AiModelConfigService.getNativeModelTools — tool-based native search', () => {
let service: AiModelConfigService;
let sdkProviderFactory: {
getRawXaiProvider: jest.Mock;
getRawAnthropicProvider: jest.Mock;
getRawOpenAIProvider: jest.Mock;
};
const xaiModel: RegisteredAiModel = {
modelId: 'xai/grok-4',
sdkPackage: AI_SDK_XAI,
model: {} as RegisteredAiModel['model'],
providerName: 'xai',
};
beforeEach(async () => {
sdkProviderFactory = {
getRawXaiProvider: jest.fn().mockReturnValue({
tools: {
webSearch: jest.fn().mockReturnValue(XAI_WEB_TOOL),
xSearch: jest.fn().mockReturnValue(XAI_X_TOOL),
},
}),
getRawAnthropicProvider: jest.fn().mockReturnValue({
tools: {
webSearch_20250305: jest.fn().mockReturnValue(ANTHROPIC_WEB_TOOL),
},
}),
getRawOpenAIProvider: jest.fn().mockReturnValue({
tools: {
webSearch: jest.fn().mockReturnValue(OPENAI_WEB_TOOL),
},
}),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AiModelConfigService,
{
provide: AiModelRegistryService,
useValue: {},
},
{
provide: SdkProviderFactoryService,
useValue: {},
},
{ provide: AiModelRegistryService, useValue: {} },
{ provide: SdkProviderFactoryService, useValue: sdkProviderFactory },
],
}).compile();
service = module.get<AiModelConfigService>(AiModelConfigService);
});
it('returns empty options when neither webSearch nor twitterSearch is enabled', () => {
it('binds no tools when neither webSearch nor twitterSearch is enabled', () => {
expect(
service.getNativeModelBinding(xaiModel, {
service.getNativeModelTools(xaiModel, {
webSearch: false,
twitterSearch: false,
}).providerOptions,
}),
).toEqual({});
});
it('omits sources entirely when neither flag is enabled (no implicit "auto" search)', () => {
const result = service.getNativeModelBinding(xaiModel, {}).providerOptions;
expect(result).toEqual({});
it('binds no tools when no options are passed (no implicit "auto" search)', () => {
expect(service.getNativeModelTools(xaiModel, {})).toEqual({});
});
it('emits only the web source when only webSearch is enabled', () => {
expect(
service.getNativeModelBinding(xaiModel, {
webSearch: true,
twitterSearch: false,
}).providerOptions,
).toEqual({
xai: {
searchParameters: {
mode: 'auto',
sources: [{ type: 'web' }],
},
},
it('binds only the xAI web_search tool when only webSearch is enabled', () => {
const tools = service.getNativeModelTools(xaiModel, {
webSearch: true,
twitterSearch: false,
});
expect(tools).toEqual({ web_search: XAI_WEB_TOOL });
});
it('emits only the x source when only twitterSearch is enabled', () => {
expect(
service.getNativeModelBinding(xaiModel, {
webSearch: false,
twitterSearch: true,
}).providerOptions,
).toEqual({
xai: {
searchParameters: {
mode: 'auto',
sources: [{ type: 'x' }],
},
},
it('binds only the xAI x_search tool when only twitterSearch is enabled', () => {
const tools = service.getNativeModelTools(xaiModel, {
webSearch: false,
twitterSearch: true,
});
expect(tools).toEqual({ x_search: XAI_X_TOOL });
});
it('emits both sources when webSearch and twitterSearch are enabled', () => {
expect(
service.getNativeModelBinding(xaiModel, {
webSearch: true,
twitterSearch: true,
}).providerOptions,
).toEqual({
xai: {
searchParameters: {
mode: 'auto',
sources: [{ type: 'web' }, { type: 'x' }],
},
},
});
});
it('preserves source order — web before x — for deterministic provider payloads', () => {
const result = service.getNativeModelBinding(xaiModel, {
it('binds both xAI tools when webSearch and twitterSearch are enabled', () => {
const tools = service.getNativeModelTools(xaiModel, {
webSearch: true,
twitterSearch: true,
}).providerOptions;
});
expect(result).toMatchObject({
xai: {
searchParameters: {
sources: [{ type: 'web' }, { type: 'x' }],
expect(tools).toEqual({
web_search: XAI_WEB_TOOL,
x_search: XAI_X_TOOL,
});
});
it('binds no tools when the xAI provider cannot be resolved', () => {
sdkProviderFactory.getRawXaiProvider.mockReturnValueOnce(undefined);
expect(
service.getNativeModelTools(xaiModel, {
webSearch: true,
twitterSearch: true,
}),
).toEqual({});
});
it('binds no tools when the model has no resolved providerName', () => {
const modelWithoutProvider: RegisteredAiModel = {
modelId: 'xai/grok-4',
sdkPackage: AI_SDK_XAI,
model: {} as RegisteredAiModel['model'],
};
expect(
service.getNativeModelTools(modelWithoutProvider, {
webSearch: true,
twitterSearch: true,
}),
).toEqual({});
expect(sdkProviderFactory.getRawXaiProvider).not.toHaveBeenCalled();
});
it('binds the Anthropic web_search tool when webSearch is enabled', () => {
const anthropicModel: RegisteredAiModel = {
modelId: 'anthropic/claude-sonnet-4-6',
sdkPackage: AI_SDK_ANTHROPIC,
model: {} as RegisteredAiModel['model'],
providerName: 'anthropic',
};
expect(
service.getNativeModelTools(anthropicModel, { webSearch: true }),
).toEqual({ web_search: ANTHROPIC_WEB_TOOL });
});
it('binds the OpenAI web_search tool when webSearch is enabled', () => {
const openaiModel: RegisteredAiModel = {
modelId: 'openai/gpt-4.1',
sdkPackage: AI_SDK_OPENAI,
model: {} as RegisteredAiModel['model'],
providerName: 'openai',
};
expect(
service.getNativeModelTools(openaiModel, { webSearch: true }),
).toEqual({ web_search: OPENAI_WEB_TOOL });
});
});
describe('AiModelConfigService.getReasoningProviderOptions', () => {
let service: AiModelConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AiModelConfigService,
{ provide: AiModelRegistryService, useValue: {} },
{ provide: SdkProviderFactoryService, useValue: {} },
],
}).compile();
service = module.get<AiModelConfigService>(AiModelConfigService);
});
it('enables Anthropic thinking for reasoning-capable models', () => {
const anthropicModel: RegisteredAiModel = {
modelId: 'anthropic/claude-sonnet-4-6',
sdkPackage: AI_SDK_ANTHROPIC,
model: {} as RegisteredAiModel['model'],
supportsReasoning: true,
};
expect(service.getReasoningProviderOptions(anthropicModel)).toEqual({
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: AGENT_CONFIG.REASONING_BUDGET_TOKENS,
},
},
});
});
it('returns empty options for non-xAI models even when search flags are on', () => {
it('returns empty options for non-reasoning Anthropic models', () => {
const anthropicModel: RegisteredAiModel = {
modelId: 'anthropic/claude-sonnet-4-6',
modelId: 'anthropic/claude-haiku',
sdkPackage: AI_SDK_ANTHROPIC,
model: {} as RegisteredAiModel['model'],
supportsReasoning: false,
};
expect(
service.getNativeModelBinding(anthropicModel, {
webSearch: true,
twitterSearch: true,
}).providerOptions,
).toEqual({});
expect(service.getReasoningProviderOptions(anthropicModel)).toEqual({});
});
it('returns empty options for xAI models (no reasoning provider options)', () => {
const xaiModel: RegisteredAiModel = {
modelId: 'xai/grok-4',
sdkPackage: AI_SDK_XAI,
model: {} as RegisteredAiModel['model'],
supportsReasoning: true,
};
expect(service.getReasoningProviderOptions(xaiModel)).toEqual({});
});
});
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { type ProviderOptions } from '@ai-sdk/provider-utils';
import { type ToolSet } from 'ai';
@@ -11,34 +11,21 @@ import {
AI_SDK_OPENAI,
AI_SDK_XAI,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-sdk-package.const';
import { getNativeModelToolsForSdkPackage } from 'src/engine/metadata-modules/ai/ai-models/utils/get-native-model-tools-for-sdk-package.util';
import {
AiModelRegistryService,
RegisteredAiModel,
} from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { type NativeModelBinding } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-binding.type';
import { type NativeModelToolOptions } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-tool-options.type';
import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-models/services/sdk-provider-factory.service';
import { type NativeModelToolOptions } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-tool-options.type';
import { getNativeModelToolsForSdkPackage } from 'src/engine/metadata-modules/ai/ai-models/utils/get-native-model-tools-for-sdk-package.util';
@Injectable()
export class AiModelConfigService {
private readonly logger = new Logger(AiModelConfigService.name);
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly sdkProviderFactory: SdkProviderFactoryService,
) {}
getNativeModelBinding(
model: RegisteredAiModel,
options: NativeModelToolOptions = {},
): NativeModelBinding {
return {
tools: this.getNativeModelTools(model, options),
providerOptions: this.getNativeSearchProviderOptions(model, options),
};
}
getReasoningProviderOptions(model: RegisteredAiModel): ProviderOptions {
switch (model.sdkPackage) {
case AI_SDK_ANTHROPIC:
@@ -50,54 +37,65 @@ export class AiModelConfigService {
}
}
private getNativeModelTools(
getNativeModelTools(
model: RegisteredAiModel,
options: NativeModelToolOptions,
options: NativeModelToolOptions = {},
): ToolSet {
const tools: Record<string, unknown> = {};
if (options.webSearch !== true) {
return tools as ToolSet;
}
const nativeTools = getNativeModelToolsForSdkPackage(model.sdkPackage);
const providerName = model.providerName;
const webSearchTool = getNativeModelToolsForSdkPackage(
model.sdkPackage,
)?.webSearch;
if (!isDefined(webSearchTool)) {
this.logger.warn(
`webSearch requested for sdkPackage="${model.sdkPackage}" but no native binding is registered. Skipping.`,
);
return tools as ToolSet;
}
// provider-option bindings (e.g. xAI) are handled in getNativeSearchProviderOptions
if (webSearchTool.kind !== 'sdk-tool') {
if (!isDefined(nativeTools) || !isDefined(providerName)) {
return tools as ToolSet;
}
switch (model.sdkPackage) {
case AI_SDK_ANTHROPIC: {
const anthropicProvider = model.providerName
? this.sdkProviderFactory.getRawAnthropicProvider(model.providerName)
: undefined;
if (options.webSearch === true && isDefined(nativeTools.webSearch)) {
const anthropicProvider =
this.sdkProviderFactory.getRawAnthropicProvider(providerName);
if (anthropicProvider) {
tools[webSearchTool.directToolName] =
anthropicProvider.tools.webSearch_20250305();
if (isDefined(anthropicProvider)) {
tools[nativeTools.webSearch.directToolName] =
anthropicProvider.tools.webSearch_20250305();
}
}
break;
}
case AI_SDK_OPENAI: {
const openaiProvider = model.providerName
? this.sdkProviderFactory.getRawOpenAIProvider(model.providerName)
: undefined;
if (options.webSearch === true && isDefined(nativeTools.webSearch)) {
const openaiProvider =
this.sdkProviderFactory.getRawOpenAIProvider(providerName);
if (openaiProvider) {
tools[webSearchTool.directToolName] =
openaiProvider.tools.webSearch();
if (isDefined(openaiProvider)) {
tools[nativeTools.webSearch.directToolName] =
openaiProvider.tools.webSearch();
}
}
break;
}
case AI_SDK_XAI: {
const xaiProvider =
this.sdkProviderFactory.getRawXaiProvider(providerName);
if (!isDefined(xaiProvider)) {
break;
}
if (options.webSearch === true && isDefined(nativeTools.webSearch)) {
tools[nativeTools.webSearch.directToolName] =
xaiProvider.tools.webSearch();
}
if (
options.twitterSearch === true &&
isDefined(nativeTools.twitterSearch)
) {
tools[nativeTools.twitterSearch.directToolName] =
xaiProvider.tools.xSearch();
}
break;
@@ -107,63 +105,6 @@ export class AiModelConfigService {
return tools as ToolSet;
}
private getNativeSearchProviderOptions(
model: RegisteredAiModel,
options: NativeModelToolOptions,
): ProviderOptions {
switch (model.sdkPackage) {
case AI_SDK_XAI:
return this.getXaiSearchProviderOptions(options);
default:
return {};
}
}
private getXaiSearchProviderOptions(
options: NativeModelToolOptions,
): ProviderOptions {
const webSearchEnabled = options.webSearch === true;
const twitterSearchEnabled = options.twitterSearch === true;
if (!webSearchEnabled && !twitterSearchEnabled) {
return {};
}
const sources: Array<{ type: string }> = [];
const xaiTools = getNativeModelToolsForSdkPackage(AI_SDK_XAI);
const webSearchTool = xaiTools?.webSearch;
const twitterSearchTool = xaiTools?.twitterSearch;
if (webSearchEnabled) {
if (webSearchTool?.kind === 'provider-option') {
sources.push({ type: webSearchTool.providerOptionKey });
} else {
this.logger.warn(
`webSearch requested for xAI but no provider-option binding is registered. Skipping.`,
);
}
}
if (twitterSearchEnabled) {
if (twitterSearchTool?.kind === 'provider-option') {
sources.push({ type: twitterSearchTool.providerOptionKey });
} else {
this.logger.warn(
`twitterSearch requested for xAI but no provider-option binding is registered. Skipping.`,
);
}
}
return {
xai: {
searchParameters: {
mode: 'auto',
...(sources.length > 0 && { sources }),
},
},
};
}
private getAnthropicProviderOptions(
model: RegisteredAiModel,
): ProviderOptions {
@@ -1,12 +1,10 @@
import { type ToolSet } from 'ai';
import { type RegisteredAiModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { type NativeModelBinding } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-binding.type';
import { type NativeModelToolOptions } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-tool-options.type';
// Parallel to ToolProvider — resolves the complete native-model call payload
// (SDK-native tools + provider options), not registry descriptors.
// Parallel to ToolProvider — resolves the SDK-native tools the model can call
// directly (web search, X search, …), not registry descriptors.
export interface NativeToolBinder {
bind(
model: RegisteredAiModel,
options: NativeModelToolOptions,
): NativeModelBinding;
bind(model: RegisteredAiModel, options: NativeModelToolOptions): ToolSet;
}
@@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { AiModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-config.service';
import { type RegisteredAiModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { type NativeToolBinder } from 'src/engine/metadata-modules/ai/ai-models/services/native-tool-binder.interface';
import { type NativeModelBinding } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-binding.type';
import { type NativeModelToolOptions } from 'src/engine/metadata-modules/ai/ai-models/types/native-model-tool-options.type';
@Injectable()
@@ -13,7 +14,7 @@ export class NativeToolBinderService implements NativeToolBinder {
bind(
model: RegisteredAiModel,
options: NativeModelToolOptions = {},
): NativeModelBinding {
return this.aiModelConfigService.getNativeModelBinding(model, options);
): ToolSet {
return this.aiModelConfigService.getNativeModelTools(model, options);
}
}
@@ -7,7 +7,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createMistral } from '@ai-sdk/mistral';
import { createOpenAI, type OpenAIProvider } from '@ai-sdk/openai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { createXai } from '@ai-sdk/xai';
import { createXai, type XaiProvider } from '@ai-sdk/xai';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { type LanguageModel } from 'ai';
import { type AiSdkPackage } from 'twenty-shared/ai';
@@ -75,6 +75,10 @@ export class SdkProviderFactoryService {
return this.getRawProvider<OpenAIProvider>(providerName, AI_SDK_OPENAI);
}
getRawXaiProvider(providerName: string): XaiProvider | undefined {
return this.getRawProvider<XaiProvider>(providerName, AI_SDK_XAI);
}
clearCache(): void {
this.providerInstances.clear();
}
@@ -92,7 +96,7 @@ export class SdkProviderFactoryService {
case AI_SDK_MISTRAL:
return this.buildStandardProvider(config, createMistral);
case AI_SDK_XAI:
return this.buildStandardProvider(config, createXai);
return this.buildXaiProvider(config);
case AI_SDK_BEDROCK:
return this.buildBedrockProvider(config);
case AI_SDK_OPENAI_COMPATIBLE:
@@ -121,6 +125,19 @@ export class SdkProviderFactoryService {
};
}
private buildXaiProvider(config: AiProviderConfig): AiSdkProviderInstance {
const provider = createXai({
...(config.apiKey && { apiKey: config.apiKey }),
...(config.baseUrl && { baseURL: config.baseUrl }),
});
return {
createModel: (modelId: string) => provider.responses(modelId),
rawProvider: provider,
sdkPackage: AI_SDK_XAI,
};
}
private buildBedrockProvider(
config: AiProviderConfig,
): AiSdkProviderInstance {
@@ -1,7 +0,0 @@
import { type ProviderOptions } from '@ai-sdk/provider-utils';
import { type ToolSet } from 'ai';
export type NativeModelBinding = {
tools: ToolSet;
providerOptions: ProviderOptions;
};
@@ -3,13 +3,9 @@ import { type NativeModelToolKey } from 'src/engine/metadata-modules/ai/ai-model
export type NativeModelTools = Partial<
Record<
NativeModelToolKey,
| {
kind: 'sdk-tool';
directToolName: string;
}
| {
kind: 'provider-option';
providerOptionKey: string;
}
{
kind: 'sdk-tool';
directToolName: string;
}
>
>;