diff --git a/apps/web/__tests__/components/settings/SearchConfigSection_test.ts b/apps/web/__tests__/components/settings/SearchConfigSection_test.ts index c71aead86..aec534eae 100644 --- a/apps/web/__tests__/components/settings/SearchConfigSection_test.ts +++ b/apps/web/__tests__/components/settings/SearchConfigSection_test.ts @@ -16,7 +16,7 @@ const { default: SearchConfigSection } = await import('../../../src/components/s const config: SearchConfig = { provider: 'tavily', tavily: { apiKey: 'key' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index 988ba74d0..6b4d9aea7 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -355,9 +355,9 @@ export interface ControlPlaneModel extends PublicModel { } export interface SearchConfig { - provider: 'disabled' | 'tavily' | 'microsoft-grounding' | 'jina'; + provider: 'disabled' | 'tavily' | 'microsoft-web-iq' | 'jina'; tavily: { apiKey: string }; - microsoftGrounding: { apiKey: string }; + microsoftWebIq: { apiKey: string }; jina: { apiKey: string }; passthroughOpenAiSearch: { enabled: boolean; upstreamId: string; model: string }; } diff --git a/apps/web/src/components/settings/ImportSection.vue b/apps/web/src/components/settings/ImportSection.vue index 79b3ea3f4..c4a574f74 100644 --- a/apps/web/src/components/settings/ImportSection.vue +++ b/apps/web/src/components/settings/ImportSection.vue @@ -20,7 +20,7 @@ interface ExportPayload { // The dashboard only round-trips the current export format. Older exports are // rejected rather than silently coerced. -const EXPORT_VERSION = 17 as const; +const EXPORT_VERSION = 18 as const; const api = useApi(); diff --git a/apps/web/src/components/settings/SearchConfigSection.vue b/apps/web/src/components/settings/SearchConfigSection.vue index 6b11b39f7..3aef808d1 100644 --- a/apps/web/src/components/settings/SearchConfigSection.vue +++ b/apps/web/src/components/settings/SearchConfigSection.vue @@ -44,11 +44,11 @@ const PROVIDER_OPTIONS: ProviderOption[] = [ set: (config, apiKey) => ({ ...config, tavily: { apiKey } }), }, { - value: 'microsoft-grounding', - label: 'Microsoft Grounding', - description: 'Gateway-managed Microsoft Grounding key.', - apiKey: config => config.microsoftGrounding.apiKey, - set: (config, apiKey) => ({ ...config, microsoftGrounding: { apiKey } }), + value: 'microsoft-web-iq', + label: 'Microsoft Web IQ', + description: 'Gateway-managed Microsoft Web IQ API key.', + apiKey: config => config.microsoftWebIq.apiKey, + set: (config, apiKey) => ({ ...config, microsoftWebIq: { apiKey } }), }, { value: 'jina', diff --git a/apps/web/src/pages/dashboard/settings.vue b/apps/web/src/pages/dashboard/settings.vue index fe87877b7..0f3294c04 100644 --- a/apps/web/src/pages/dashboard/settings.vue +++ b/apps/web/src/pages/dashboard/settings.vue @@ -23,7 +23,7 @@ import { useUpstreamsStore } from '../../composables/useUpstreams.ts'; const defaultSearchConfig: SearchConfig = { provider: 'disabled', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; diff --git a/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts b/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts index 71c5a0146..196b8b69e 100644 --- a/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts +++ b/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts @@ -218,7 +218,7 @@ const WEB_SEARCH_USAGE_1: WebSearchUsageRecord = { }; const WEB_SEARCH_USAGE_2: WebSearchUsageRecord = { - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', keyId: 'key-b', action: 'fetch_page', hour: '2026-01-01T11', @@ -290,7 +290,7 @@ const doExport = async (app: Hono, includePerformance = false) => { return (await resp.json()) as Record; }; -const doImport = async (app: Hono, mode: string, data: unknown, version: unknown = 17) => { +const doImport = async (app: Hono, mode: string, data: unknown, version: unknown = 18) => { const resp = await app.request('/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -334,7 +334,7 @@ test('export emits the v17 envelope with users and upstreams', async () => { const result = await doExport(app); - assertEquals(result.version, 17); + assertEquals(result.version, 18); assertEquals(typeof result.exportedAt, 'string'); assertEquals(result.data.users, [SEED_ADMIN]); assertEquals(result.data.apiKeys, []); @@ -361,7 +361,7 @@ test('export includes full upstream configs and omits performance by default', a await repo.webSearchConfig.save({ provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -399,7 +399,7 @@ test('import rejects any version other than the current one before deleting data await repo.apiKeys.save(KEY_A); await repo.upstreams.save(CUSTOM_UPSTREAM); - const VERSION_ERROR = 'version must be 17 — older export formats are not supported; re-export from the current deployment'; + const VERSION_ERROR = 'version must be 18 — older export formats are not supported; re-export from the current deployment'; const previousV11 = await doImport(app, 'replace', latestImportData(), 11); const ancientVersion = await doImport(app, 'replace', { apiKeys: [] }, 1); const missingVersionResponse = await app.request('/import', { @@ -429,7 +429,7 @@ test('import replace writes upstreams and clears replaced collections', async () await repo.webSearchConfig.save({ provider: 'tavily', tavily: { apiKey: 'old' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -442,9 +442,9 @@ test('import replace writes upstreams and clears replaced collections', async () searchUsage: [WEB_SEARCH_USAGE_2], performanceIncluded: false, searchConfig: { - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: 'ms-new' }, + microsoftWebIq: { apiKey: 'ms-new' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }, @@ -460,9 +460,9 @@ test('import replace writes upstreams and clears replaced collections', async () assertEquals(await repo.webSearchUsage.listAll(), [WEB_SEARCH_USAGE_2]); assertEquals(await repo.responsesItems.lookupMany('key-a', [STORED_RESPONSES_ITEM.id], 0), []); assertEquals(await repo.webSearchConfig.get(), { - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: 'ms-new' }, + microsoftWebIq: { apiKey: 'ms-new' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -987,7 +987,7 @@ test('import rejects legacy enabled_fixes payloads before mutating', async () => assertEquals(await repo.upstreams.list(), [CUSTOM_UPSTREAM]); }); -test('import rejects missing latest-v17 arrays before clearing existing data', async () => { +test('import rejects missing latest-v18 arrays before clearing existing data', async () => { const { app, repo } = setup(); await repo.apiKeys.save(KEY_A); await repo.upstreams.save(CUSTOM_UPSTREAM); @@ -1013,14 +1013,14 @@ test('import rejects missing latest-v17 arrays before clearing existing data', a test('import validates mode and data before mutating', async () => { const { app } = setup(); - const invalidMode = await doImport(app, 'invalid', {}, 17); + const invalidMode = await doImport(app, 'invalid', {}, 18); const missingData = await app.request('/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode: 'replace', version: 17 }), + body: JSON.stringify({ mode: 'replace', version: 18 }), }); - const missingUpstreams = await doImport(app, 'merge', {}, 17); - const emptyMerge = await doImport(app, 'merge', latestImportData(), 17); + const missingUpstreams = await doImport(app, 'merge', {}, 18); + const emptyMerge = await doImport(app, 'merge', latestImportData(), 18); assertEquals(invalidMode.status, 400); assertEquals(invalidMode.body.error, "mode must be 'merge' or 'replace'"); @@ -1180,10 +1180,10 @@ test('v17 export/import round-trips users and per-key user_id', async () => { await repo.apiKeys.save({ ...KEY_B, userId: USER_BOB.id }); const exportResult = await doExport(app); - assertEquals(exportResult.version, 17); + assertEquals(exportResult.version, 18); assertEquals(exportResult.data.users.map((u: any) => u.id).sort(), [SEED_ADMIN.id, USER_BOB.id]); - const result = await doImport(app, 'replace', exportResult.data, 17); + const result = await doImport(app, 'replace', exportResult.data, 18); assertEquals(result.status, 200); assertEquals(result.body.imported.users, 2); assertEquals(result.body.imported.apiKeys, 2); @@ -1206,7 +1206,7 @@ test('v17 import rejects api_keys whose user_id does not appear in the payload', searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(result.status, 400); assertEquals(result.body.error, 'invalid apiKeys at index 0: user_id 99 does not match any user in the payload'); @@ -1223,7 +1223,7 @@ test('v17 import rejects malformed users (bad username, bad password_hash)', asy searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(badUsername.status, 400); assertEquals(String(badUsername.body.error).startsWith('invalid users at index 0:'), true); @@ -1235,7 +1235,7 @@ test('v17 import rejects malformed users (bad username, bad password_hash)', asy searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(badHash.status, 400); assertEquals(String(badHash.body.error).includes('passwordHash'), true); }); @@ -1257,7 +1257,7 @@ test('import rejects a pre-accounts v3 export instead of coercing its legacy api }, 3); assertEquals(result.status, 400); - assertEquals(String(result.body.error).includes('version must be 17'), true); + assertEquals(String(result.body.error).includes('version must be 18'), true); // Rejected at the version gate, before touching any data. assertEquals(await repo.apiKeys.list(), [KEY_A]); assertEquals((await repo.users.list()).map(u => u.id), [SEED_ADMIN.id]); @@ -1278,7 +1278,7 @@ test('replace-mode import clears sessions before writing users', async () => { searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(result.status, 200); // No public listAll on sessions; create a fresh session and check the @@ -1297,7 +1297,7 @@ test('v17 import rejects users[i].upstreamIds === undefined', async () => { searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(result.status, 400); expect(result.body.error).toMatch(/upstreamIds/); }); @@ -1312,7 +1312,7 @@ test('v17 import rejects users[i].deletedAt of non-string non-null type', async searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(result.status, 400); expect(result.body.error).toMatch(/deletedAt/); }); @@ -1327,7 +1327,7 @@ test('v17 replace import refuses payload missing user 1', async () => { searchUsage: [], performanceIncluded: false, searchConfig: DEFAULT_WEB_SEARCH_CONFIG, - }, 17); + }, 18); assertEquals(result.status, 400); expect(result.body.error).toMatch(/user 1/); }); @@ -1351,19 +1351,19 @@ test('a full v17 export re-imports verbatim — the export→import round trip i const config = { provider: 'tavily' as const, tavily: { apiKey: 'tk' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; await repo.webSearchConfig.save(config); const exported = await doExport(app, true); - assertEquals(exported.version, 17); + assertEquals(exported.version, 18); // Replace-import the export's own `data`, verbatim. If the export emits any // shape the import parser rejects, this 400s — the round trip is the // invariant, so this test fails the moment the two sides drift. - const result = await doImport(app, 'replace', exported.data, 17); + const result = await doImport(app, 'replace', exported.data, 18); assertEquals(result.status, 200); assertEquals(result.body.imported, { users: 2, apiKeys: 2, upstreams: 4, proxies: 0, usage: 2, searchUsage: 2, performance: 2 }); @@ -1399,7 +1399,7 @@ test('any data bearing a historical version is rejected on the version gate, bef for (const version of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) { const result = await doImport(app, 'replace', wellFormed, version); assertEquals(result.status, 400); - assertEquals(String(result.body.error).includes('version must be 17'), true); + assertEquals(String(result.body.error).includes('version must be 18'), true); } // Nothing was touched — the version gate runs before any delete or write. diff --git a/packages/gateway/__tests__/control-plane/search-config/routes_test.ts b/packages/gateway/__tests__/control-plane/search-config/routes_test.ts index 6dbc730c1..49810258f 100644 --- a/packages/gateway/__tests__/control-plane/search-config/routes_test.ts +++ b/packages/gateway/__tests__/control-plane/search-config/routes_test.ts @@ -53,7 +53,7 @@ test('/api/search-config PUT persists config and POST /test returns preview', as const config = { provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: 'jina-test' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; diff --git a/packages/gateway/__tests__/control-plane/search-usage/routes_test.ts b/packages/gateway/__tests__/control-plane/search-usage/routes_test.ts index 97fedc93a..aa016c256 100644 --- a/packages/gateway/__tests__/control-plane/search-usage/routes_test.ts +++ b/packages/gateway/__tests__/control-plane/search-usage/routes_test.ts @@ -19,7 +19,7 @@ const seedWebSearchUsage = async (repo: import('../../repo/memory.ts').InMemoryR await repo.webSearchUsage.set({ provider: 'tavily', keyId: primaryKeyId, action: 'search', hour: '2026-03-15T10', requests: 2 }); await repo.webSearchUsage.set({ provider: 'tavily', keyId: primaryKeyId, action: 'fetch_page', hour: '2026-03-15T10', requests: 3 }); - await repo.webSearchUsage.set({ provider: 'microsoft-grounding', keyId: 'key_other', action: 'search', hour: '2026-03-15T11', requests: 4 }); + await repo.webSearchUsage.set({ provider: 'microsoft-web-iq', keyId: 'key_other', action: 'search', hour: '2026-03-15T11', requests: 4 }); }; test('/api/search-usage scopes to the actor\'s keys when called with an API key', async () => { @@ -47,9 +47,9 @@ test('/api/search-usage in self-by-key mode includes per-key metadata for the ac const { repo, apiKey } = await setupAppTest(); await seedWebSearchUsage(repo, apiKey.id); await repo.webSearchConfig.save({ - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -60,7 +60,7 @@ test('/api/search-usage in self-by-key mode includes per-key metadata for the ac assertEquals(response.status, 200); const body = await response.json(); - assertEquals(body.activeProvider, 'microsoft-grounding'); + assertEquals(body.activeProvider, 'microsoft-web-iq'); assertEquals(body.keys, [ { id: apiKey.id, name: apiKey.name, createdAt: apiKey.createdAt }, ]); @@ -86,11 +86,11 @@ test('/api/search-usage all-by-user view aggregates across keys per user', async assertEquals(response.status, 200); const body = await response.json(); - // Two distinct rows: tavily/user2 (apiKey.userId === 2) and microsoft-grounding/user1. + // Two distinct rows: tavily/user2 (apiKey.userId === 2) and microsoft-web-iq/user1. // Aggregation summed both actions for the tavily row. Sort order is hour, userId, provider. assertEquals(body, [ { provider: 'tavily', userId: 2, hour: '2026-03-15T10', requests: 5 }, - { provider: 'microsoft-grounding', userId: 1, hour: '2026-03-15T11', requests: 4 }, + { provider: 'microsoft-web-iq', userId: 1, hour: '2026-03-15T11', requests: 4 }, ]); }); diff --git a/packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts b/packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts index 9c9f5229b..d57c0b961 100644 --- a/packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts +++ b/packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts @@ -20,7 +20,7 @@ const mockResolveConfigured = vi.mocked(resolveConfiguredWebSearchProvider); const TAVILY_CONFIG: WebSearchConfig = { provider: 'tavily', tavily: { apiKey: 'test-key' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; diff --git a/packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts b/packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts index d9e3c7a9a..bea8a6fe4 100644 --- a/packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts +++ b/packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts @@ -295,7 +295,7 @@ beforeEach(() => { void repo.webSearchConfig.save({ provider: 'tavily', tavily: { apiKey: 'test-key' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, } satisfies WebSearchConfig); @@ -3945,7 +3945,7 @@ test('responses target with OpenAI passthrough forwards the complete alpha-searc await getRepo().webSearchConfig.save({ provider: 'tavily', tavily: { apiKey: 'test-key' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: true, upstreamId: 'up_codex', model: 'gpt-search' }, } satisfies WebSearchConfig); @@ -4012,7 +4012,7 @@ test('local and cascaded Floway unsupported commands produce the same agent-visi await getRepo().webSearchConfig.save({ provider: 'tavily', tavily: { apiKey: 'test-key' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: true, upstreamId: 'up_floway', model: 'gpt-search' }, } satisfies WebSearchConfig); diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts index d511eb86d..7899b5818 100644 --- a/packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts +++ b/packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts @@ -5,26 +5,27 @@ import type { WebSearchConfig } from '../../../../src/data-plane/tools/web-searc import { initRepo } from '../../../../src/repo/index.ts'; import { SqlRepo } from '../../../../src/repo/sql.ts'; import { InMemoryRepo } from '../../../repo/memory.ts'; +import { createSqliteTestDb } from '../../../repo/test-sqlite.ts'; import type { SqlDatabase } from '@floway-dev/platform'; import { assertEquals, assertRejects, assertThrows } from '@floway-dev/test-utils'; interface WebSearchConfigRow { provider: string; tavily_api_key: string; - microsoft_grounding_api_key: string; + microsoft_web_iq_api_key: string; jina_api_key: string; passthrough_openai_search: number; alpha_search_upstream_id: string; alpha_search_model: string; } -const SELECT_SQL = 'SELECT provider, tavily_api_key, microsoft_grounding_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model FROM search_config WHERE id = 1'; -const UPSERT_SQL = `INSERT INTO search_config (id, provider, tavily_api_key, microsoft_grounding_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model, updated_at) +const SELECT_SQL = 'SELECT provider, tavily_api_key, microsoft_web_iq_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model FROM search_config WHERE id = 1'; +const UPSERT_SQL = `INSERT INTO search_config (id, provider, tavily_api_key, microsoft_web_iq_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ON CONFLICT (id) DO UPDATE SET provider = excluded.provider, tavily_api_key = excluded.tavily_api_key, - microsoft_grounding_api_key = excluded.microsoft_grounding_api_key, + microsoft_web_iq_api_key = excluded.microsoft_web_iq_api_key, jina_api_key = excluded.jina_api_key, passthrough_openai_search = excluded.passthrough_openai_search, alpha_search_upstream_id = excluded.alpha_search_upstream_id, @@ -58,7 +59,7 @@ class FakeSqlPreparedStatement { this.db.webSearchConfig = { provider: String(this.binds[0]), tavily_api_key: String(this.binds[1]), - microsoft_grounding_api_key: String(this.binds[2]), + microsoft_web_iq_api_key: String(this.binds[2]), jina_api_key: String(this.binds[3]), passthrough_openai_search: Number(this.binds[4]), alpha_search_upstream_id: String(this.binds[5]), @@ -90,7 +91,7 @@ test('search config repo defaults to disabled and round-trips provider keys', as await saveWebSearchConfig({ provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: 'jina-test' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -98,7 +99,7 @@ test('search config repo defaults to disabled and round-trips provider keys', as assertEquals(await loadWebSearchConfig(), { provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: 'jina-test' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -112,7 +113,7 @@ test('loadWebSearchConfig strict-parses a stored row and rejects unknown provide await repo.webSearchConfig.save({ provider: 'unknown-provider', tavily: { apiKey: ' tvly-test ' }, - microsoftGrounding: { apiKey: ' ms-test ' }, + microsoftWebIq: { apiKey: ' ms-test ' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, } as unknown as WebSearchConfig); @@ -127,7 +128,7 @@ test('loadWebSearchConfig strict-parses a stored row and trims valid api keys', await repo.webSearchConfig.save({ provider: 'jina', tavily: { apiKey: ' tvly-trim ' }, - microsoftGrounding: { apiKey: ' ms-trim ' }, + microsoftWebIq: { apiKey: ' ms-trim ' }, jina: { apiKey: ' jina-trim ' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -135,7 +136,7 @@ test('loadWebSearchConfig strict-parses a stored row and trims valid api keys', assertEquals(await loadWebSearchConfig(), { provider: 'jina', tavily: { apiKey: 'tvly-trim' }, - microsoftGrounding: { apiKey: 'ms-trim' }, + microsoftWebIq: { apiKey: 'ms-trim' }, jina: { apiKey: 'jina-trim' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -155,15 +156,15 @@ test('parseWebSearchConfigStrict throws on missing required fields', () => { assertThrows( () => parseWebSearchConfigStrict({ provider: 'disabled', tavily: { apiKey: '' } }), Error, - 'microsoftGrounding', + 'microsoftWebIq', ); assertThrows( - () => parseWebSearchConfigStrict({ provider: 'disabled', tavily: {}, microsoftGrounding: { apiKey: '' }, jina: { apiKey: '' } }), + () => parseWebSearchConfigStrict({ provider: 'disabled', tavily: {}, microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' } }), Error, 'tavily.apiKey', ); assertThrows( - () => parseWebSearchConfigStrict({ provider: 'disabled', tavily: { apiKey: '' }, microsoftGrounding: { apiKey: '' } }), + () => parseWebSearchConfigStrict({ provider: 'disabled', tavily: { apiKey: '' }, microsoftWebIq: { apiKey: '' } }), Error, 'jina', ); @@ -183,7 +184,7 @@ test('saveWebSearchConfig writes the typed columns and round-trips through the s const saved = await saveWebSearchConfig({ provider: 'disabled', tavily: { apiKey: ' tvly-test ' }, - microsoftGrounding: { apiKey: ' ms-test ' }, + microsoftWebIq: { apiKey: ' ms-test ' }, jina: { apiKey: ' jina-test ' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -191,14 +192,14 @@ test('saveWebSearchConfig writes the typed columns and round-trips through the s assertEquals(saved, { provider: 'disabled', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: 'jina-test' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); assertEquals(db.webSearchConfig, { provider: 'disabled', tavily_api_key: 'tvly-test', - microsoft_grounding_api_key: 'ms-test', + microsoft_web_iq_api_key: 'ms-test', jina_api_key: 'jina-test', passthrough_openai_search: 0, alpha_search_upstream_id: '', @@ -207,8 +208,38 @@ test('saveWebSearchConfig writes the typed columns and round-trips through the s assertEquals(await loadWebSearchConfig(), { provider: 'disabled', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: 'jina-test' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); }); + +test('current SQL schema round-trips Microsoft Web IQ configuration and usage', async () => { + const repo = new SqlRepo(await createSqliteTestDb()); + initRepo(repo); + + const config: WebSearchConfig = { + provider: 'microsoft-web-iq', + tavily: { apiKey: '' }, + microsoftWebIq: { apiKey: 'web-iq-test' }, + jina: { apiKey: '' }, + passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, + }; + await repo.webSearchConfig.save(config); + assertEquals(await loadWebSearchConfig(), config); + + await repo.webSearchUsage.record({ + provider: 'microsoft-web-iq', + keyId: 'key-a', + action: 'search', + hour: '2026-07-29T00', + requests: 2, + }); + assertEquals(await repo.webSearchUsage.listAll(), [{ + provider: 'microsoft-web-iq', + keyId: 'key-a', + action: 'search', + hour: '2026-07-29T00', + requests: 2, + }]); +}); diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts index 5b73d70e6..068d74c33 100644 --- a/packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts +++ b/packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts @@ -13,7 +13,7 @@ test('normalizeDomainEntry rejects empty, whitespace-only, single-label, or non- assertEquals(normalizeDomainEntry(''), null); assertEquals(normalizeDomainEntry(' '), null); assertEquals(normalizeDomainEntry('localhost'), null); - // Query-operator smuggling (the original Microsoft Grounding regression). + // Query-operator smuggling (the original Microsoft Web IQ regression). assertEquals(normalizeDomainEntry('example.com OR site:evil.com'), null); assertEquals(normalizeDomainEntry('bad.com test'), null); assertEquals(normalizeDomainEntry('https://example.com'), null); @@ -34,7 +34,7 @@ test('normalizeDomainList drops invalid entries and keeps valid ones in order', // Cross-site parity: the same input must be treated identically by the // local URL-allowed filter, the Tavily request builder, and the -// Microsoft Grounding query builder. All three route through these +// Microsoft Web IQ query builder. All three route through these // helpers; this test pins down the contract. test('normalizeDomainEntry parity contract for the three call sites', () => { const input = ' Example.COM '; diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts index 07c1d95f2..00225ef63 100644 --- a/packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts +++ b/packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts @@ -15,7 +15,7 @@ test('resolveConfiguredWebSearchProvider returns disabled, missing-credential, o resolveConfiguredWebSearchProvider({ provider: 'tavily', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }), @@ -26,9 +26,9 @@ test('resolveConfiguredWebSearchProvider returns disabled, missing-credential, o ); const resolved = resolveConfiguredWebSearchProvider({ - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -37,7 +37,7 @@ test('resolveConfiguredWebSearchProvider returns disabled, missing-credential, o if (resolved.type !== 'enabled') { throw new Error('expected enabled provider'); } - assertEquals(resolved.provider, 'microsoft-grounding'); + assertEquals(resolved.provider, 'microsoft-web-iq'); }); test('testWebSearchConfigConnection returns structured disabled and missing-credential errors', async () => { @@ -55,7 +55,7 @@ test('testWebSearchConfigConnection returns structured disabled and missing-cred await testWebSearchConfigConnection({ provider: 'tavily', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }), @@ -103,7 +103,7 @@ test('testWebSearchConfigConnection previews at most three normalized results', const result = await testWebSearchConfigConnection({ provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -133,7 +133,7 @@ test('testWebSearchConfigConnection returns no_results when the provider returns await testWebSearchConfigConnection({ provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }), @@ -151,7 +151,7 @@ test('testWebSearchConfigConnection returns no_results when the provider returns ); }); -test('testWebSearchConfigConnection returns preview results for Microsoft Grounding too', async () => { +test('testWebSearchConfigConnection returns preview results for Microsoft Web IQ too', async () => { await withMockedFetch( () => jsonResponse({ @@ -166,9 +166,9 @@ test('testWebSearchConfigConnection returns preview results for Microsoft Ground }), async () => { const result = await testWebSearchConfigConnection({ - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: 'ms-test' }, + microsoftWebIq: { apiKey: 'ms-test' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); @@ -178,7 +178,7 @@ test('testWebSearchConfigConnection returns preview results for Microsoft Ground throw new Error('expected a successful Microsoft preview result'); } - assertEquals(result.provider, 'microsoft-grounding'); + assertEquals(result.provider, 'microsoft-web-iq'); assertEquals(result.query, FIXED_WEB_SEARCH_CONFIG_TEST_QUERY); assertEquals(result.results, [ { @@ -211,7 +211,7 @@ test('testWebSearchConfigConnection does not record search usage', async () => { const result = await testWebSearchConfigConnection({ provider: 'tavily', tavily: { apiKey: 'tvly-test' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }); diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-grounding_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-web-iq_test.ts similarity index 78% rename from packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-grounding_test.ts rename to packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-web-iq_test.ts index 32e360734..628b33bf1 100644 --- a/packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-grounding_test.ts +++ b/packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-web-iq_test.ts @@ -1,10 +1,10 @@ import { test } from 'vitest'; -import { createMicrosoftGroundingWebSearchProvider } from '../../../../../src/data-plane/tools/web-search/providers/microsoft-grounding.ts'; +import { createMicrosoftWebIqWebSearchProvider } from '../../../../../src/data-plane/tools/web-search/providers/microsoft-web-iq.ts'; import { FakeTime } from '../../../../test-time.ts'; import { assertEquals, jsonResponse, withMockedFetch } from '@floway-dev/test-utils'; -test('createMicrosoftGroundingWebSearchProvider calls v3 search/web with passage content', async () => { +test('createMicrosoftWebIqWebSearchProvider calls v3 search/web with passage content', async () => { let request: Request | undefined; await withMockedFetch( @@ -22,7 +22,7 @@ test('createMicrosoftGroundingWebSearchProvider calls v3 search/web with passage }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const result = await provider.search({ query: 'React documentation', allowedDomains: ['react.dev', 'example.com OR site:evil.com'], @@ -42,14 +42,14 @@ test('createMicrosoftGroundingWebSearchProvider calls v3 search/web with passage assertEquals(body.region, 'GB'); assertEquals(result.type, 'ok'); if (result.type !== 'ok') { - throw new Error('expected successful Microsoft Grounding result'); + throw new Error('expected successful Microsoft Web IQ result'); } assertEquals(result.results[0].pageAge, '2026-04-01T00:00:00Z'); }, ); }); -test('createMicrosoftGroundingWebSearchProvider forwards maxResults to upstream count', async () => { +test('createMicrosoftWebIqWebSearchProvider forwards maxResults to upstream count', async () => { let request: Request | undefined; await withMockedFetch( incoming => { @@ -57,7 +57,7 @@ test('createMicrosoftGroundingWebSearchProvider forwards maxResults to upstream return jsonResponse({ webResults: [] }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); await provider.search({ query: 'React documentation', maxResults: 4 }); const body = JSON.parse(await request!.text()); assertEquals(body.count, 4); @@ -65,7 +65,7 @@ test('createMicrosoftGroundingWebSearchProvider forwards maxResults to upstream ); }); -test('createMicrosoftGroundingWebSearchProvider rejects blank and overlong queries before fetch', async () => { +test('createMicrosoftWebIqWebSearchProvider rejects blank and overlong queries before fetch', async () => { let called = false; await withMockedFetch( @@ -74,7 +74,7 @@ test('createMicrosoftGroundingWebSearchProvider rejects blank and overlong queri return jsonResponse({ webResults: [] }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); assertEquals(await provider.search({ query: ' ' }), { type: 'error', @@ -93,7 +93,7 @@ test('createMicrosoftGroundingWebSearchProvider rejects blank and overlong queri assertEquals(called, false); }); -test('createMicrosoftGroundingWebSearchProvider retries 429 with by-design 1s/2s/4s/8s backoff and ignores retryAfter when the next attempt succeeds', async () => { +test('createMicrosoftWebIqWebSearchProvider retries 429 with by-design 1s/2s/4s/8s backoff and ignores retryAfter when the next attempt succeeds', async () => { const fakeTime = new FakeTime(); const attemptTimes: number[] = []; let attempts = 0; @@ -119,7 +119,7 @@ test('createMicrosoftGroundingWebSearchProvider retries 429 with by-design 1s/2s }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const resultPromise = provider.search({ query: 'React documentation' }); fakeTime.runMicrotasks(); @@ -150,7 +150,7 @@ test('createMicrosoftGroundingWebSearchProvider retries 429 with by-design 1s/2s } }); -test('createMicrosoftGroundingWebSearchProvider returns too_many_requests after four by-design 429 retries and ignores retryAfter', async () => { +test('createMicrosoftWebIqWebSearchProvider returns too_many_requests after four by-design 429 retries and ignores retryAfter', async () => { const fakeTime = new FakeTime(); const attemptTimes: number[] = []; @@ -161,7 +161,7 @@ test('createMicrosoftGroundingWebSearchProvider returns too_many_requests after return jsonResponse({ message: 'rate limited', retryAfter: '60s' }, 429); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const resultPromise = provider.search({ query: 'React documentation' }); fakeTime.runMicrotasks(); @@ -195,11 +195,11 @@ test('createMicrosoftGroundingWebSearchProvider returns too_many_requests after } }); -test('createMicrosoftGroundingWebSearchProvider maps 413 to request_too_large', async () => { +test('createMicrosoftWebIqWebSearchProvider maps 413 to request_too_large', async () => { await withMockedFetch( () => jsonResponse({ message: 'too large' }, 413), async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); assertEquals(await provider.search({ query: 'React documentation' }), { type: 'error', errorCode: 'request_too_large', @@ -209,11 +209,11 @@ test('createMicrosoftGroundingWebSearchProvider maps 413 to request_too_large', ); }); -test('createMicrosoftGroundingWebSearchProvider surfaces malformed payload as an error', async () => { +test('createMicrosoftWebIqWebSearchProvider surfaces malformed payload as an error', async () => { await withMockedFetch( () => jsonResponse({ message: 'unexpected' }), async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const result = await provider.search({ query: 'React documentation' }); assertEquals(result.type, 'error'); if (result.type !== 'error') throw new Error('expected error'); @@ -222,7 +222,7 @@ test('createMicrosoftGroundingWebSearchProvider surfaces malformed payload as an ); }); -test('Microsoft Grounding fetchPage issues one /v3/browse call per URL in parallel', async () => { +test('Microsoft Web IQ fetchPage issues one /v3/browse call per URL in parallel', async () => { const callBodies: Array> = []; await withMockedFetch( @@ -237,7 +237,7 @@ test('Microsoft Grounding fetchPage issues one /v3/browse call per URL in parall }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const result = await provider.fetchPage({ urls: ['https://a.com', 'https://b.com'] }); assertEquals(callBodies.length, 2); @@ -260,11 +260,11 @@ test('Microsoft Grounding fetchPage issues one /v3/browse call per URL in parall ); }); -test('Microsoft Grounding fetchPage treats HTTP 202 as a per-URL failure (cold cache)', async () => { +test('Microsoft Web IQ fetchPage treats HTTP 202 as a per-URL failure (cold cache)', async () => { await withMockedFetch( () => jsonResponse({ retryAfter: '30' }, 202), async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const result = await provider.fetchPage({ urls: ['https://cold.com'] }); if (result.type !== 'ok') throw new Error('expected ok'); assertEquals(result.failures, [{ url: 'https://cold.com', errorCode: 'unavailable', message: 'live crawl pending' }]); @@ -273,7 +273,7 @@ test('Microsoft Grounding fetchPage treats HTTP 202 as a per-URL failure (cold c ); }); -test('Microsoft Grounding fetchPage truncates long pages to MAX_FETCH_PAGE_BYTES', async () => { +test('Microsoft Web IQ fetchPage truncates long pages to MAX_FETCH_PAGE_BYTES', async () => { const long = 'y'.repeat(30_000); await withMockedFetch( async incoming => { @@ -281,7 +281,7 @@ test('Microsoft Grounding fetchPage truncates long pages to MAX_FETCH_PAGE_BYTES return jsonResponse({ url: body.url, title: 'T', content: long }); }, async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const result = await provider.fetchPage({ urls: ['https://a.com'] }); if (result.type !== 'ok') throw new Error('expected ok'); assertEquals(result.pages[0].truncated, true); @@ -291,13 +291,13 @@ test('Microsoft Grounding fetchPage truncates long pages to MAX_FETCH_PAGE_BYTES ); }); -test('Microsoft Grounding fetchPage returns whole-batch error on HTTP 5xx after retry exhaustion', async () => { +test('Microsoft Web IQ fetchPage returns whole-batch error on HTTP 5xx after retry exhaustion', async () => { const fakeTime = new FakeTime(); try { await withMockedFetch( () => new Response('upstream broken', { status: 502 }), async () => { - const provider = createMicrosoftGroundingWebSearchProvider('ms-test'); + const provider = createMicrosoftWebIqWebSearchProvider('ms-test'); const resultPromise = provider.fetchPage({ urls: ['https://a.com'] }); await fakeTime.tickAsync(15_000); const result = await resultPromise; diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts index 49716d2d8..ee4e94b92 100644 --- a/packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts +++ b/packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts @@ -35,7 +35,7 @@ test('runWebSearchAndRecordUsage records provider error results', async () => { initRepo(repo); const result = await runWebSearchAndRecordUsage({ - providerName: 'microsoft-grounding', + providerName: 'microsoft-web-iq', keyId: 'key_b', request: { query: 'React' }, provider: stubProvider(() => @@ -49,7 +49,7 @@ test('runWebSearchAndRecordUsage records provider error results', async () => { assertEquals(result.type, 'error'); const records = await repo.webSearchUsage.listAll(); assertEquals(records.length, 1); - assertEquals(records[0].provider, 'microsoft-grounding'); + assertEquals(records[0].provider, 'microsoft-web-iq'); assertEquals(records[0].keyId, 'key_b'); assertEquals(records[0].requests, 1); }); diff --git a/packages/gateway/__tests__/repo/search-usage_test.ts b/packages/gateway/__tests__/repo/search-usage_test.ts index 90fe08ba0..69bf66488 100644 --- a/packages/gateway/__tests__/repo/search-usage_test.ts +++ b/packages/gateway/__tests__/repo/search-usage_test.ts @@ -16,7 +16,7 @@ const exerciseWebSearchUsageRepo = async (repo: WebSearchUsageRepo) => { await repo.deleteAll(); await repo.record({ provider: 'tavily', keyId: 'key_a', action: 'search', hour: '2026-04-25T10', requests: 1 }); await repo.record({ provider: 'tavily', keyId: 'key_a', action: 'search', hour: '2026-04-25T10', requests: 2 }); - await repo.record({ provider: 'microsoft-grounding', keyId: 'key_a', action: 'search', hour: '2026-04-25T11', requests: 4 }); + await repo.record({ provider: 'microsoft-web-iq', keyId: 'key_a', action: 'search', hour: '2026-04-25T11', requests: 4 }); await repo.record({ provider: 'tavily', keyId: 'key_b', action: 'search', hour: '2026-04-25T12', requests: 8 }); await repo.record({ provider: 'tavily', keyId: 'key_a', action: 'search', hour: '2026-04-25T13', requests: 16 }); @@ -59,7 +59,7 @@ const exerciseWebSearchUsageRepo = async (repo: WebSearchUsageRepo) => { requests: 3, }, { - provider: 'microsoft-grounding', + provider: 'microsoft-web-iq', keyId: 'key_a', action: 'search', hour: '2026-04-25T11', diff --git a/packages/gateway/migrations/0069_microsoft_web_iq.sql b/packages/gateway/migrations/0069_microsoft_web_iq.sql new file mode 100644 index 000000000..7a77b0c1d --- /dev/null +++ b/packages/gateway/migrations/0069_microsoft_web_iq.sql @@ -0,0 +1,35 @@ +-- Rename the Microsoft web-search provider and preserve every stored +-- credential and usage bucket. The configuration table has no provider CHECK, +-- while search_usage does, so the latter needs the standard SQLite table swap. + +ALTER TABLE search_config RENAME COLUMN microsoft_grounding_api_key TO microsoft_web_iq_api_key; + +UPDATE search_config +SET provider = 'microsoft-web-iq' +WHERE provider = 'microsoft-grounding'; + +CREATE TABLE search_usage_new ( + provider TEXT NOT NULL CHECK (provider IN ('tavily', 'microsoft-web-iq', 'jina')), + key_id TEXT NOT NULL, + action TEXT NOT NULL DEFAULT 'search' CHECK (action IN ('search', 'fetch_page')), + hour TEXT NOT NULL, + requests INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (provider, key_id, action, hour) +); + +INSERT INTO search_usage_new (provider, key_id, action, hour, requests) +SELECT + CASE provider + WHEN 'microsoft-grounding' THEN 'microsoft-web-iq' + ELSE provider + END, + key_id, + action, + hour, + requests +FROM search_usage; + +DROP INDEX IF EXISTS idx_search_usage_hour; +DROP TABLE search_usage; +ALTER TABLE search_usage_new RENAME TO search_usage; +CREATE INDEX idx_search_usage_hour ON search_usage (hour); diff --git a/packages/gateway/src/control-plane/data-transfer/routes.ts b/packages/gateway/src/control-plane/data-transfer/routes.ts index c4e09d341..2268c8583 100644 --- a/packages/gateway/src/control-plane/data-transfer/routes.ts +++ b/packages/gateway/src/control-plane/data-transfer/routes.ts @@ -47,7 +47,7 @@ interface SerializedProxy { } interface ExportPayload { - version: 17; + version: 18; exportedAt: string; data: { users: User[]; @@ -62,7 +62,7 @@ interface ExportPayload { }; } -const EXPORT_VERSION = 17; +const EXPORT_VERSION = 18; const SEARCH_USAGE_HOUR_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}$/; const PERFORMANCE_METRICS = new Set(['ttft_ms', 'tpot_us']); const UPSTREAM_PROVIDERS = new Set(ALL_PROVIDER_KINDS); diff --git a/packages/gateway/src/control-plane/schemas.ts b/packages/gateway/src/control-plane/schemas.ts index d7ca3e017..026564887 100644 --- a/packages/gateway/src/control-plane/schemas.ts +++ b/packages/gateway/src/control-plane/schemas.ts @@ -558,9 +558,9 @@ export const resetBackoffBody = z.object({ // --- search config --- export const webSearchConfigSchema = z.object({ - provider: z.enum(['disabled', 'tavily', 'microsoft-grounding', 'jina']), + provider: z.enum(['disabled', 'tavily', 'microsoft-web-iq', 'jina']), tavily: z.object({ apiKey: z.string() }), - microsoftGrounding: z.object({ apiKey: z.string() }), + microsoftWebIq: z.object({ apiKey: z.string() }), jina: z.object({ apiKey: z.string() }), passthroughOpenAiSearch: z.object({ enabled: z.boolean(), @@ -699,7 +699,7 @@ export const updateAliasBody = aliasBodyCore.superRefine(aliasBodyRulesRefinemen // --- data transfer --- export const importBody = z.object({ - version: z.literal(17, { error: 'version must be 17 — older export formats are not supported; re-export from the current deployment' }), + version: z.literal(18, { error: 'version must be 18 — older export formats are not supported; re-export from the current deployment' }), mode: z.enum(['merge', 'replace'], { error: "mode must be 'merge' or 'replace'" }), data: z.unknown().optional(), }); diff --git a/packages/gateway/src/control-plane/search-usage/routes.ts b/packages/gateway/src/control-plane/search-usage/routes.ts index ab1a520df..79131386e 100644 --- a/packages/gateway/src/control-plane/search-usage/routes.ts +++ b/packages/gateway/src/control-plane/search-usage/routes.ts @@ -8,7 +8,7 @@ import { aggregateWebSearchUsageByKey, aggregateWebSearchUsageByUser } from './a import { loadWebSearchConfig } from '../../data-plane/tools/web-search/config.ts'; import { type CtxWithQuery } from '../../middleware/zod-validator.ts'; import { getRepo } from '../../repo/index.ts'; -import { isWebSearchProviderName } from '../../shared/web-search-providers.ts'; +import { WEB_SEARCH_PROVIDER_NAMES, isWebSearchProviderName } from '../../shared/web-search-providers.ts'; import type { webSearchUsageQuery } from '../schemas.ts'; import { buildKeyToUserMap } from '../shared/key-to-user.ts'; import { resolveUsageView } from '../shared/usage-view.ts'; @@ -22,7 +22,7 @@ export const webSearchUsage = async (c: CtxWithQuery const { provider } = query; if (provider !== undefined && !isWebSearchProviderName(provider)) { - return c.json({ error: "provider must be 'tavily' or 'microsoft-grounding'" }, 400); + return c.json({ error: `provider must be one of ${WEB_SEARCH_PROVIDER_NAMES.map(name => `'${name}'`).join(', ')}` }, 400); } const resolved = resolveUsageView(c, query.view, query.key_id); diff --git a/packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts b/packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts index b6cb4ffda..eff8be655 100644 --- a/packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts +++ b/packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts @@ -57,7 +57,7 @@ const formatUserLocation = (loc: NonNullable): // The injected function mirrors the complete command object accepted by // OpenAI's alpha-search endpoint. Alpha passthrough can execute every field; -// local Tavily, Jina, and Microsoft Grounding execution rejects the fields it +// local Tavily, Jina, and Microsoft Web IQ execution rejects the fields it // cannot implement before dispatch. // https://github.com/openai/codex/blob/2f19a57704fb7b1db032bc38cf995034254eaebb/codex-rs/codex-api/src/search.rs#L31-L213 export const buildShimFunctionTool = ( diff --git a/packages/gateway/src/data-plane/tools/web-search/config.ts b/packages/gateway/src/data-plane/tools/web-search/config.ts index b5f5e29d1..14dea203b 100644 --- a/packages/gateway/src/data-plane/tools/web-search/config.ts +++ b/packages/gateway/src/data-plane/tools/web-search/config.ts @@ -6,7 +6,7 @@ import { WEB_SEARCH_PROVIDER_NAMES, isWebSearchProviderName } from '../../../sha export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = { provider: 'disabled', tavily: { apiKey: '' }, - microsoftGrounding: { apiKey: '' }, + microsoftWebIq: { apiKey: '' }, jina: { apiKey: '' }, passthroughOpenAiSearch: { enabled: false, upstreamId: '', model: '' }, }; @@ -33,11 +33,11 @@ export const parseWebSearchConfigStrict = (input: unknown): WebSearchConfig => { if (typeof input.tavily.apiKey !== 'string') { throw new Error('search config tavily.apiKey must be a string'); } - if (!isJsonObject(input.microsoftGrounding)) { - throw new Error('search config microsoftGrounding must be an object'); + if (!isJsonObject(input.microsoftWebIq)) { + throw new Error('search config microsoftWebIq must be an object'); } - if (typeof input.microsoftGrounding.apiKey !== 'string') { - throw new Error('search config microsoftGrounding.apiKey must be a string'); + if (typeof input.microsoftWebIq.apiKey !== 'string') { + throw new Error('search config microsoftWebIq.apiKey must be a string'); } if (!isJsonObject(input.jina)) { throw new Error('search config jina must be an object'); @@ -60,7 +60,7 @@ export const parseWebSearchConfigStrict = (input: unknown): WebSearchConfig => { return { provider: input.provider, tavily: { apiKey: input.tavily.apiKey.trim() }, - microsoftGrounding: { apiKey: input.microsoftGrounding.apiKey.trim() }, + microsoftWebIq: { apiKey: input.microsoftWebIq.apiKey.trim() }, jina: { apiKey: input.jina.apiKey.trim() }, passthroughOpenAiSearch: { enabled: passthrough.enabled, upstreamId, model }, }; diff --git a/packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts b/packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts index 46a790405..34bce95a8 100644 --- a/packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts +++ b/packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts @@ -1,9 +1,9 @@ // Shared normalizer for `allowed_domains` / `blocked_domains` list // entries, used by the local URL-allowed filter, the Tavily -// include/exclude payload, and the Microsoft Grounding `site:` builder. +// include/exclude payload, and the Microsoft Web IQ `site:` builder. // All three must agree on what a "domain entry" means — we unify on // the strictest of the three: trim, lowercase, validate. Entries that -// fail validation drop, matching Grounding's behavior. +// fail validation drop, matching Microsoft Web IQ's behavior. // // Pattern requires at least one dot; labels are 1-63 chars of // `[a-z0-9-]`, may not start or end with `-`. Schemes, ports, paths, diff --git a/packages/gateway/src/data-plane/tools/web-search/operations.ts b/packages/gateway/src/data-plane/tools/web-search/operations.ts index db394ec0d..a0018fc27 100644 --- a/packages/gateway/src/data-plane/tools/web-search/operations.ts +++ b/packages/gateway/src/data-plane/tools/web-search/operations.ts @@ -149,7 +149,7 @@ const assertObjectHasOnly = ( } }; -// The local Tavily, Jina, and Microsoft Grounding adapters implement the +// The local Tavily, Jina, and Microsoft Web IQ adapters implement the // common text-search/open/find subset. OpenAI's complete command object remains // valid for alpha-search passthrough; reaching a local adapter with any other // command or nested command parameter is an explicit error so no request field @@ -443,7 +443,7 @@ export const renderWebSearchCallOutput = (ir: WebSearchCallIR): string => // ── Domain filtering ── -// Suffix-match per Tavily and Microsoft Grounding search-side filter +// Suffix-match per Tavily and Microsoft Web IQ search-side filter // semantics: `example.com` matches `example.com`, `www.example.com`, and // `sub.example.com`, but NOT `evil-example.com`. const matchesAnyDomain = (hostname: string, domains: readonly string[]): boolean => { diff --git a/packages/gateway/src/data-plane/tools/web-search/provider.ts b/packages/gateway/src/data-plane/tools/web-search/provider.ts index 943da3942..c1c9faee5 100644 --- a/packages/gateway/src/data-plane/tools/web-search/provider.ts +++ b/packages/gateway/src/data-plane/tools/web-search/provider.ts @@ -1,6 +1,6 @@ import { FIXED_WEB_SEARCH_CONFIG_TEST_QUERY } from './config.ts'; import { createJinaWebSearchProvider } from './providers/jina.ts'; -import { createMicrosoftGroundingWebSearchProvider } from './providers/microsoft-grounding.ts'; +import { createMicrosoftWebIqWebSearchProvider } from './providers/microsoft-web-iq.ts'; import { createTavilyWebSearchProvider } from './providers/tavily.ts'; import type { ConfiguredWebSearchProvider, WebSearchConfig, WebSearchConfigConnectionTestResult, WebSearchProvider, WebSearchProviderName } from './types.ts'; @@ -16,7 +16,7 @@ const toPreviewText = (content: Array<{ type: 'text'; text: string }>): string = // if-branch. const PROVIDER_FACTORIES: { [N in WebSearchProviderName]: (config: WebSearchConfig) => { apiKey: string; build: (apiKey: string) => WebSearchProvider } } = { tavily: config => ({ apiKey: config.tavily.apiKey, build: createTavilyWebSearchProvider }), - 'microsoft-grounding': config => ({ apiKey: config.microsoftGrounding.apiKey, build: createMicrosoftGroundingWebSearchProvider }), + 'microsoft-web-iq': config => ({ apiKey: config.microsoftWebIq.apiKey, build: createMicrosoftWebIqWebSearchProvider }), jina: config => ({ apiKey: config.jina.apiKey, build: createJinaWebSearchProvider }), }; diff --git a/packages/gateway/src/data-plane/tools/web-search/providers/jina.ts b/packages/gateway/src/data-plane/tools/web-search/providers/jina.ts index f1f1b9d84..2a66ea973 100644 --- a/packages/gateway/src/data-plane/tools/web-search/providers/jina.ts +++ b/packages/gateway/src/data-plane/tools/web-search/providers/jina.ts @@ -19,7 +19,7 @@ const JINA_READER_URL = 'https://r.jina.ai/'; // `X-Max-Tokens` on s.jina.ai. Jina's default scrapes the full readability // markdown of each result page, which can run into double-digit KB; capping // to 500 tokens (~2 KB markdown) keeps each Jina result in the same size -// ballpark as Microsoft Grounding's `passage` mode (~300-400 tokens) and +// ballpark as Microsoft Web IQ's `passage` mode (~300-400 tokens) and // Tavily's `basic` mode (~100 tokens / ~400 chars). 500 is also Jina's // documented minimum — values below trigger `Rejected by validator (v) => // v >= 500`. Verified against jina-ai/reader's `tokenTrim` call sites in @@ -80,7 +80,7 @@ const normalizeSearchResult = (value: unknown): Extract readOneUrl(httpFetch, apiKey, url, request.signal))); // Whole-batch transport / 5xx failure collapses into one envelope — - // mirrors Microsoft Grounding's policy. Per-URL 4xx stays granular so + // mirrors Microsoft Web IQ's policy. Per-URL 4xx stays granular so // a single bad target doesn't poison the rest of the batch. const allHardFail = outcomes.every(outcome => outcome.kind === 'fail' && (outcome.httpStatus === 0 || outcome.httpStatus >= 500)); if (allHardFail) { diff --git a/packages/gateway/src/data-plane/tools/web-search/providers/microsoft-grounding.ts b/packages/gateway/src/data-plane/tools/web-search/providers/microsoft-web-iq.ts similarity index 86% rename from packages/gateway/src/data-plane/tools/web-search/providers/microsoft-grounding.ts rename to packages/gateway/src/data-plane/tools/web-search/providers/microsoft-web-iq.ts index 3a17b5a29..5dc3da659 100644 --- a/packages/gateway/src/data-plane/tools/web-search/providers/microsoft-grounding.ts +++ b/packages/gateway/src/data-plane/tools/web-search/providers/microsoft-web-iq.ts @@ -13,14 +13,17 @@ import { type WebSearchProviderResult, } from '../types.ts'; -const MICROSOFT_GROUNDING_SEARCH_URL = 'https://api.microsoft.ai/v3/search/web'; -// Grounding `browse` API is single-URL; we issue Promise.all over the batch. +// Microsoft publishes api.microsoft.ai/v3 as Web IQ's REST API base. +// https://webiq.microsoft.ai/documentation/quick-start/ +const MICROSOFT_WEB_IQ_SEARCH_URL = 'https://api.microsoft.ai/v3/search/web'; +// Microsoft Web IQ's `browse` API is single-URL; we issue Promise.all over the batch. // Per-iteration concurrency is naturally bounded by the shim's iteration cap // (~30) and the model's parallel call count (≤4 in practice). -const MICROSOFT_GROUNDING_BROWSE_URL = 'https://api.microsoft.ai/v3/browse'; +// https://webiq.microsoft.ai/documentation/quick-start/ +const MICROSOFT_WEB_IQ_BROWSE_URL = 'https://api.microsoft.ai/v3/browse'; const toMicrosoftQuery = (request: WebSearchProviderRequest, query: string) => { - // Microsoft Grounding has no allow/block-domain fields, so domain + // Microsoft Web IQ has no allow/block-domain fields, so domain // policy is biased through `site:` / `-site:` operators. Best-effort, // not strict. Smuggled query fragments (e.g. `example.com OR // site:evil.com`) get rejected at normalization. @@ -63,7 +66,7 @@ type BrowseOutcome = const browseOneUrl = async (httpFetch: typeof fetch, apiKey: string, url: string, signal?: AbortSignal): Promise => { try { - const response = await fetchWithRetry(() => httpFetch(MICROSOFT_GROUNDING_BROWSE_URL, { + const response = await fetchWithRetry(() => httpFetch(MICROSOFT_WEB_IQ_BROWSE_URL, { method: 'POST', headers: { 'content-type': 'application/json', @@ -79,7 +82,7 @@ const browseOneUrl = async (httpFetch: typeof fetch, apiKey: string, url: string ...(signal !== undefined ? { signal } : {}), }), signal); - // Grounding returns 202 with `retryAfter` when the page isn't + // Microsoft Web IQ returns 202 with `retryAfter` when the page isn't // cached and a live crawl was kicked off. Don't poll — Workers // can't afford the budget and re-issuing from the next model turn // is fine. @@ -94,7 +97,7 @@ const browseOneUrl = async (httpFetch: typeof fetch, apiKey: string, url: string const payload = await response.json(); if (!isJsonObject(payload) || typeof payload.url !== 'string') { - return { kind: 'fail', url, httpStatus: response.status, message: 'Microsoft Grounding browse returned an unexpected payload.' }; + return { kind: 'fail', url, httpStatus: response.status, message: 'Microsoft Web IQ browse returned an unexpected payload.' }; } return { kind: 'ok', @@ -112,7 +115,7 @@ const browseOneUrl = async (httpFetch: typeof fetch, apiKey: string, url: string } }; -export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: { fetch?: typeof fetch }): WebSearchProvider => { +export const createMicrosoftWebIqWebSearchProvider = (apiKey: string, deps?: { fetch?: typeof fetch }): WebSearchProvider => { const httpFetch = deps?.fetch ?? fetch; const search = async (request: WebSearchProviderRequest): Promise => { @@ -133,7 +136,7 @@ export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: } try { - const response = await fetchWithRetry(() => httpFetch(MICROSOFT_GROUNDING_SEARCH_URL, { + const response = await fetchWithRetry(() => httpFetch(MICROSOFT_WEB_IQ_SEARCH_URL, { method: 'POST', headers: { 'content-type': 'application/json', @@ -146,12 +149,12 @@ export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: if (response.ok) { const payload = await response.json(); // Unexpected payload shape is a backend contract violation; - // returning empty results would mask a real Grounding outage. + // returning empty results would mask a real Microsoft Web IQ outage. if (!isJsonObject(payload) || !Array.isArray(payload.webResults)) { return { type: 'error', errorCode: 'unavailable', - message: 'Microsoft Grounding returned an unexpected payload shape; check provider status.', + message: 'Microsoft Web IQ returned an unexpected payload shape; check provider status.', }; } const results = payload.webResults.map(normalizeResult).filter((entry): entry is NonNullable => entry !== null); @@ -168,7 +171,7 @@ export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: return { type: 'error', errorCode: httpStatusToErrorCode(response.status), - message: message ?? 'Microsoft Grounding rate limited the request.', + message: message ?? 'Microsoft Web IQ rate limited the request.', }; } @@ -176,7 +179,7 @@ export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: return { type: 'error', errorCode: httpStatusToErrorCode(response.status), - message: message ?? 'Microsoft Grounding rejected the search query.', + message: message ?? 'Microsoft Web IQ rejected the search query.', }; } @@ -184,20 +187,20 @@ export const createMicrosoftGroundingWebSearchProvider = (apiKey: string, deps?: return { type: 'error', errorCode: httpStatusToErrorCode(response.status), - message: message ?? 'Microsoft Grounding rejected the request as too large.', + message: message ?? 'Microsoft Web IQ rejected the request as too large.', }; } return { type: 'error', errorCode: httpStatusToErrorCode(response.status), - message: message ?? 'Microsoft Grounding search failed.', + message: message ?? 'Microsoft Web IQ search failed.', }; } catch (error) { return { type: 'error', errorCode: 'unavailable', - message: error instanceof Error ? error.message : 'Microsoft Grounding search failed.', + message: error instanceof Error ? error.message : 'Microsoft Web IQ search failed.', }; } }; diff --git a/packages/gateway/src/repo/sql.ts b/packages/gateway/src/repo/sql.ts index a7032ffff..8f8e292c8 100644 --- a/packages/gateway/src/repo/sql.ts +++ b/packages/gateway/src/repo/sql.ts @@ -866,13 +866,13 @@ class SqlWebSearchConfigRepo implements WebSearchConfigRepo { async get(): Promise { const row = await this.db - .prepare('SELECT provider, tavily_api_key, microsoft_grounding_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model FROM search_config WHERE id = 1') - .first<{ provider: string; tavily_api_key: string; microsoft_grounding_api_key: string; jina_api_key: string; passthrough_openai_search: number; alpha_search_upstream_id: string; alpha_search_model: string }>(); + .prepare('SELECT provider, tavily_api_key, microsoft_web_iq_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model FROM search_config WHERE id = 1') + .first<{ provider: string; tavily_api_key: string; microsoft_web_iq_api_key: string; jina_api_key: string; passthrough_openai_search: number; alpha_search_upstream_id: string; alpha_search_model: string }>(); if (!row) throw new Error('search_config singleton row missing'); return { provider: row.provider, tavily: { apiKey: row.tavily_api_key }, - microsoftGrounding: { apiKey: row.microsoft_grounding_api_key }, + microsoftWebIq: { apiKey: row.microsoft_web_iq_api_key }, jina: { apiKey: row.jina_api_key }, passthroughOpenAiSearch: { enabled: row.passthrough_openai_search === 1, @@ -883,22 +883,22 @@ class SqlWebSearchConfigRepo implements WebSearchConfigRepo { } async save(config: WebSearchConfig): Promise { - const { provider, tavily, microsoftGrounding, jina, passthroughOpenAiSearch } = config; + const { provider, tavily, microsoftWebIq, jina, passthroughOpenAiSearch } = config; await this.db .prepare( - `INSERT INTO search_config (id, provider, tavily_api_key, microsoft_grounding_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model, updated_at) + `INSERT INTO search_config (id, provider, tavily_api_key, microsoft_web_iq_api_key, jina_api_key, passthrough_openai_search, alpha_search_upstream_id, alpha_search_model, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ON CONFLICT (id) DO UPDATE SET provider = excluded.provider, tavily_api_key = excluded.tavily_api_key, - microsoft_grounding_api_key = excluded.microsoft_grounding_api_key, + microsoft_web_iq_api_key = excluded.microsoft_web_iq_api_key, jina_api_key = excluded.jina_api_key, passthrough_openai_search = excluded.passthrough_openai_search, alpha_search_upstream_id = excluded.alpha_search_upstream_id, alpha_search_model = excluded.alpha_search_model, updated_at = excluded.updated_at`, ) - .bind(provider, tavily.apiKey, microsoftGrounding.apiKey, jina.apiKey, passthroughOpenAiSearch.enabled ? 1 : 0, passthroughOpenAiSearch.upstreamId, passthroughOpenAiSearch.model) + .bind(provider, tavily.apiKey, microsoftWebIq.apiKey, jina.apiKey, passthroughOpenAiSearch.enabled ? 1 : 0, passthroughOpenAiSearch.upstreamId, passthroughOpenAiSearch.model) .run(); } } diff --git a/packages/gateway/src/shared/web-search-providers.ts b/packages/gateway/src/shared/web-search-providers.ts index cecbfddfa..7551f235e 100644 --- a/packages/gateway/src/shared/web-search-providers.ts +++ b/packages/gateway/src/shared/web-search-providers.ts @@ -1,11 +1,11 @@ -export const WEB_SEARCH_PROVIDER_NAMES = ['tavily', 'microsoft-grounding', 'jina'] as const; +export const WEB_SEARCH_PROVIDER_NAMES = ['tavily', 'microsoft-web-iq', 'jina'] as const; export type WebSearchProviderName = (typeof WEB_SEARCH_PROVIDER_NAMES)[number]; export interface WebSearchConfig { provider: 'disabled' | WebSearchProviderName; tavily: { apiKey: string }; - microsoftGrounding: { apiKey: string }; + microsoftWebIq: { apiKey: string }; jina: { apiKey: string }; passthroughOpenAiSearch: { enabled: boolean;