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:
+1
-1
@@ -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({}),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+6
-9
@@ -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`);
|
||||
|
||||
+7
-8
@@ -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();
|
||||
|
||||
|
||||
+4
-4
@@ -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]: {},
|
||||
|
||||
+157
-73
@@ -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({});
|
||||
});
|
||||
});
|
||||
|
||||
+45
-104
@@ -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 {
|
||||
|
||||
+5
-7
@@ -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;
|
||||
}
|
||||
|
||||
+4
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -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 {
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { type ProviderOptions } from '@ai-sdk/provider-utils';
|
||||
import { type ToolSet } from 'ai';
|
||||
|
||||
export type NativeModelBinding = {
|
||||
tools: ToolSet;
|
||||
providerOptions: ProviderOptions;
|
||||
};
|
||||
+4
-8
@@ -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;
|
||||
}
|
||||
>
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user