From ea14a4539409a2dd495c8b8eb68b488e5ac4e2ed Mon Sep 17 00:00:00 2001 From: thesujai Date: Mon, 21 Sep 2026 18:42:18 +0530 Subject: [PATCH 01/10] feat: add web search provider in standalone and OIDC --- .changeset/oss-web-search-providers.md | 6 + packages/trueforge/.env.example | 3 + .../trueforge/catalog/web-search-catalog.yaml | 3 + .../trueforge/scripts/generate-catalog.mjs | 3 +- packages/trueforge/scripts/write-openapi.ts | 5 + packages/trueforge/src/apis/agents.ts | 7 + packages/trueforge/src/apis/capabilities.ts | 9 +- packages/trueforge/src/apis/catalog.ts | 8 ++ packages/trueforge/src/apis/schedules.ts | 3 + packages/trueforge/src/apis/sessions.ts | 6 + packages/trueforge/src/apis/settings.ts | 11 ++ packages/trueforge/src/apis/turns.ts | 10 +- .../trueforge/src/apis/webSearchProviders.ts | 122 +++++++++++++++++ packages/trueforge/src/app.ts | 12 ++ .../trueforge/src/catalog/WebSearchCatalog.ts | 25 ++++ .../src/catalog/webSearchCatalog.gen.ts | 2 + packages/trueforge/src/config.ts | 7 + .../20260921_000001_web_search_provider.ts | 19 +++ packages/trueforge/src/db/postgres/types.ts | 10 ++ .../PostgresWebSearchProviderStore.ts | 128 ++++++++++++++++++ .../20260921_000001_web_search_provider.ts | 22 +++ packages/trueforge/src/db/sqlite/types.ts | 10 ++ .../SqliteWebSearchProviderStore.ts | 127 +++++++++++++++++ .../src/db/webSearchProviderStore.ts | 59 ++++++++ packages/trueforge/src/main.ts | 29 ++++ .../trueforge/src/routes/catalogRoutes.ts | 22 +++ packages/trueforge/src/routes/openapiTags.ts | 2 + .../src/routes/webSearchProviderRoutes.ts | 101 ++++++++++++++ .../trueforge/src/runtime/sessionResources.ts | 16 ++- .../trueforge/src/schemas/webSearchCatalog.ts | 22 +++ .../src/schemas/webSearchProvider.ts | 75 ++++++++++ .../TrueFoundryWebSearchProviderStore.ts | 76 +++++++++++ packages/trueforge/src/websearch/providers.ts | 33 +++-- .../trueforge/tests/unit/apis/agents.test.ts | 3 + .../tests/unit/apis/capabilities.test.ts | 17 +++ .../unit/apis/deletedSessionCrud.test.ts | 4 + .../tests/unit/apis/mcpServers.test.ts | 2 + .../tests/unit/apis/modelProviders.test.ts | 4 + .../unit/apis/sandboxFileDownload.test.ts | 2 + .../tests/unit/apis/sandboxProviders.test.ts | 2 + .../tests/unit/apis/schedules.test.ts | 1 + .../tests/unit/apis/sessionHttp.test.ts | 3 + .../trueforge/tests/unit/apis/skills.test.ts | 2 + .../unit/apis/turnHarnessErrorStatus.test.ts | 2 + .../trueforge/tests/unit/apis/turns.test.ts | 7 + .../unit/runtime/sessionResources.test.ts | 19 +-- 46 files changed, 1031 insertions(+), 30 deletions(-) create mode 100644 .changeset/oss-web-search-providers.md create mode 100644 packages/trueforge/catalog/web-search-catalog.yaml create mode 100644 packages/trueforge/src/apis/webSearchProviders.ts create mode 100644 packages/trueforge/src/catalog/WebSearchCatalog.ts create mode 100644 packages/trueforge/src/catalog/webSearchCatalog.gen.ts create mode 100644 packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts create mode 100644 packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts create mode 100644 packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts create mode 100644 packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts create mode 100644 packages/trueforge/src/db/webSearchProviderStore.ts create mode 100644 packages/trueforge/src/routes/webSearchProviderRoutes.ts create mode 100644 packages/trueforge/src/schemas/webSearchCatalog.ts create mode 100644 packages/trueforge/src/schemas/webSearchProvider.ts create mode 100644 packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts diff --git a/.changeset/oss-web-search-providers.md b/.changeset/oss-web-search-providers.md new file mode 100644 index 000000000..518737b21 --- /dev/null +++ b/.changeset/oss-web-search-providers.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge": minor +"@truefoundry/trueforge-core": minor +--- + +Add OSS web-search provider settings and catalog (Parallel): named `web_search_provider` rows, settings/catalog APIs, and tenant-aware host resolution so built-in web search works outside TrueFoundry mode. diff --git a/packages/trueforge/.env.example b/packages/trueforge/.env.example index f6c7ad775..5bdd29fdc 100644 --- a/packages/trueforge/.env.example +++ b/packages/trueforge/.env.example @@ -35,6 +35,9 @@ PORT=8790 ## When unset, the catalog inlined at build time (catalog/sandbox-catalog.yaml) is used. # SANDBOX_CATALOG_PATH=/path/to/sandbox-catalog.yaml +## Optional override for the shipped web-search catalog (discovery presets). +# WEB_SEARCH_CATALOG_PATH=/path/to/web-search-catalog.yaml + ## Frontend build the server serves alongside the API. Optional; defaults to ## packages/trueforge/dist/_frontend (after build) or monorepo packages/frontend/dist. ## With no build there the server logs a warning and serves the API only. diff --git a/packages/trueforge/catalog/web-search-catalog.yaml b/packages/trueforge/catalog/web-search-catalog.yaml new file mode 100644 index 000000000..bb319b3f9 --- /dev/null +++ b/packages/trueforge/catalog/web-search-catalog.yaml @@ -0,0 +1,3 @@ +providers: + - type: parallel + mode: turbo diff --git a/packages/trueforge/scripts/generate-catalog.mjs b/packages/trueforge/scripts/generate-catalog.mjs index cba9c04dc..f9d550c0c 100644 --- a/packages/trueforge/scripts/generate-catalog.mjs +++ b/packages/trueforge/scripts/generate-catalog.mjs @@ -1,6 +1,6 @@ /** * Inlines catalog/*.yaml into generated TS modules so ModelCatalog / McpCatalog / - * SkillCatalog / SandboxCatalog can load shipped catalogs with no loose YAML + * SkillCatalog / SandboxCatalog / WebSearchCatalog can load shipped catalogs with no loose YAML * beside the bundle. Plain generated `.ts` files work unchanged across tsup and tsx. * * Runs via the `build:gen` script before build/typecheck/test/dev. Generated @@ -24,3 +24,4 @@ writeShippedCatalog('model-catalog.yaml', 'shippedModelCatalogYaml', 'modelCatal writeShippedCatalog('mcp-catalog.yaml', 'shippedMcpCatalogYaml', 'mcpCatalog.gen.ts'); writeShippedCatalog('skill-catalog.yaml', 'shippedSkillCatalogYaml', 'skillCatalog.gen.ts'); writeShippedCatalog('sandbox-catalog.yaml', 'shippedSandboxCatalogYaml', 'sandboxCatalog.gen.ts'); +writeShippedCatalog('web-search-catalog.yaml', 'shippedWebSearchCatalogYaml', 'webSearchCatalog.gen.ts'); diff --git a/packages/trueforge/scripts/write-openapi.ts b/packages/trueforge/scripts/write-openapi.ts index 69c19657d..078c1c727 100644 --- a/packages/trueforge/scripts/write-openapi.ts +++ b/packages/trueforge/scripts/write-openapi.ts @@ -20,6 +20,7 @@ import { McpCatalog } from '../src/catalog/McpCatalog'; import { ModelCatalog } from '../src/catalog/ModelCatalog'; import { SandboxCatalog } from '../src/catalog/SandboxCatalog'; import { SkillCatalog } from '../src/catalog/SkillCatalog'; +import { WebSearchCatalog } from '../src/catalog/WebSearchCatalog'; import configuration from '../src/config'; import { McpServerWithAuthStore } from '../src/db/McpServerWithAuthStore'; import { SqliteAgentStore } from '../src/db/sqlite/agent-store/SqliteAgentStore'; @@ -31,6 +32,7 @@ import { SqliteScheduleStore } from '../src/db/sqlite/schedule-store/SqliteSched import { SqliteSessionMetricsStore } from '../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; import { SqliteSkillStore } from '../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../src/runtime/event-subscription'; @@ -64,11 +66,13 @@ const tokenStore = new SqliteOAuthTokenStore(db); const agentStore = new SqliteAgentStore(db); const skillStore = new SqliteSkillStore(db); const sandboxProviderStore = new SqliteSandboxProviderStore(db); +const webSearchProviderStore = new SqliteWebSearchProviderStore(db); const app = createServerApp({ modelCatalog: ModelCatalog.load(), mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), resolveModelProviderStore: () => new SqliteModelProviderStore(db), resolveMcpServerStore: () => new McpServerWithAuthStore({ @@ -78,6 +82,7 @@ const app = createServerApp({ }), resolveSkillStore: () => skillStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, resolveAgentStore: () => agentStore, resolveImportAgentStore: () => agentStore, agentStore, diff --git a/packages/trueforge/src/apis/agents.ts b/packages/trueforge/src/apis/agents.ts index fafe32daf..f5d25ae21 100644 --- a/packages/trueforge/src/apis/agents.ts +++ b/packages/trueforge/src/apis/agents.ts @@ -18,6 +18,7 @@ import type { IModelProviderStore } from '../db/modelProviderStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import type { ISkillStore } from '../db/skillStore'; import type { WithTransaction } from '../db/transaction'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { createAgentRoute, deleteAgentRoute, @@ -38,6 +39,7 @@ export interface AgentsRouterDeps { resolveMcpServerStore: (c: Context) => IMcpServerStore; resolveSkillStore: ResolveSkillStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; withTransaction: WithTransaction; resolveRequestContext: ResolveRequestContext; authorizer: Authorizer; @@ -60,6 +62,7 @@ async function validateManifest({ mcpServerStore, skillStore, sandboxProviderStore, + webSearchProviderStore, tenant_id, }: { spec: AgentSpec; @@ -67,6 +70,7 @@ async function validateManifest({ mcpServerStore: IMcpServerStore; skillStore: ISkillStore; sandboxProviderStore: ISandboxProviderStore; + webSearchProviderStore: IWebSearchProviderStore; tenant_id: string; }): Promise { await validateAgentSpec({ @@ -76,6 +80,7 @@ async function validateManifest({ mcpServerStore, skillStore, sandboxProviderStore, + webSearchProviderStore, }); return spec; } @@ -112,6 +117,7 @@ export function createAgentsRouter(deps: AgentsRouterDeps(deps: AgentsRouterDeps(deps: { resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; withTransaction: WithTransaction; logger: Logger; resolveRequestContext: ResolveRequestContext; @@ -46,7 +48,10 @@ export function createCapabilitiesRouter(deps: { } const sandboxEnabled = status === 'ready' || (status === undefined && isLocalSandboxFallbackEnabled()); const settingsEnabled = hasAdminRole(requestContext); - const webSearchEnabled = resolveWebSearchProvider() !== undefined; + const webSearchEnabled = await hasConfiguredWebSearchProvider({ + tenant_id: requestContext.tenant_id, + store: deps.resolveWebSearchProviderStore(c), + }); return c.json( { data: { diff --git a/packages/trueforge/src/apis/catalog.ts b/packages/trueforge/src/apis/catalog.ts index 3a2bb2c65..212344d57 100644 --- a/packages/trueforge/src/apis/catalog.ts +++ b/packages/trueforge/src/apis/catalog.ts @@ -4,11 +4,13 @@ import type { McpCatalog } from '../catalog/McpCatalog'; import type { ModelCatalog } from '../catalog/ModelCatalog'; import type { SandboxCatalog } from '../catalog/SandboxCatalog'; import type { SkillCatalog } from '../catalog/SkillCatalog'; +import type { WebSearchCatalog } from '../catalog/WebSearchCatalog'; import { listMcpServerCatalogRoute, listModelProviderCatalogRoute, listSandboxProviderCatalogRoute, listSkillCatalogRoute, + listWebSearchProviderCatalogRoute, } from '../routes/catalogRoutes'; import type { CatalogModelProvider } from '../schemas/modelCatalog'; @@ -17,6 +19,7 @@ export interface CatalogRouterDeps { mcpCatalog: McpCatalog; skillCatalog: SkillCatalog; sandboxCatalog: SandboxCatalog; + webSearchCatalog: WebSearchCatalog; } export function createCatalogRouter(deps: CatalogRouterDeps) { @@ -43,10 +46,15 @@ export function createCatalogRouter(deps: CatalogRouterDeps) { return c.json({ data: [...deps.sandboxCatalog.list()] }, 200); }; + const listWebSearchProvidersHandler: RouteHandler = c => { + return c.json({ data: [...deps.webSearchCatalog.list()] }, 200); + }; + const router = new OpenAPIHono(); router.openapi(listModelProviderCatalogRoute, listModelProvidersHandler); router.openapi(listMcpServerCatalogRoute, listMcpServersHandler); router.openapi(listSkillCatalogRoute, listSkillsHandler); router.openapi(listSandboxProviderCatalogRoute, listSandboxProvidersHandler); + router.openapi(listWebSearchProviderCatalogRoute, listWebSearchProvidersHandler); return router; } diff --git a/packages/trueforge/src/apis/schedules.ts b/packages/trueforge/src/apis/schedules.ts index da3dcc62d..1a652ecbc 100644 --- a/packages/trueforge/src/apis/schedules.ts +++ b/packages/trueforge/src/apis/schedules.ts @@ -39,6 +39,7 @@ import { } from '../db/scheduleStore'; import type { ISkillStore } from '../db/skillStore'; import type { WithTransaction } from '../db/transaction'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { createScheduleRoute, createScheduleRunRoute, @@ -72,6 +73,7 @@ export interface ScheduleTurnExecutionDeps { resolveModelProviderStore: (c: Context, runAsAgent?: AgentRecord) => IModelProviderStore; resolveMcpServerStore: (c: Context, runAsAgent?: AgentRecord) => IMcpServerWithAuthStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; /** Persistence agent store (schedule agent binding is not caller-scoped). */ agentStore: IAgentStore; turnSkillsResolverStore: Pick; @@ -115,6 +117,7 @@ export async function startScheduleRunOnRequest(params: { modelProviderStore: deps.resolveModelProviderStore(c, prepared.agent), mcpServerStore: deps.resolveMcpServerStore(c, prepared.agent), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + webSearchProviderStore: deps.resolveWebSearchProviderStore(c), skillStore: deps.turnSkillsResolverStore, logger: deps.logger, }, diff --git a/packages/trueforge/src/apis/sessions.ts b/packages/trueforge/src/apis/sessions.ts index 37083e58b..d238c48e0 100644 --- a/packages/trueforge/src/apis/sessions.ts +++ b/packages/trueforge/src/apis/sessions.ts @@ -28,6 +28,7 @@ import type { IAgentStore } from '../db/agentStore'; import type { IMcpServerStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { cancelSessionRoute, createSessionRoute, @@ -80,6 +81,7 @@ export interface SessionsRouterDeps { resolveSkillStore: ResolveSkillStore; resolveAgentStore: (c: Context) => IAgentStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; redis?: RedisClientType | undefined; requestReplyRouter: RequestReplyRouter; resolveRequestContext: ResolveRequestContext; @@ -236,6 +238,7 @@ type InternalSessionsRouterDeps = Pick< | 'resolveSkillStore' | 'resolveAgentStore' | 'resolveSandboxProviderStore' + | 'resolveWebSearchProviderStore' | 'resolveRequestContext' | 'authorizer' >; @@ -289,6 +292,7 @@ function createGetOrCreateSessionByExternalIdHandler( mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + webSearchProviderStore: deps.resolveWebSearchProviderStore(c), }); agent = { type: 'inline', spec: body.agent.spec }; } @@ -361,6 +365,7 @@ export function createSessionsRouter(deps: SessionsRouterDeps) { mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + webSearchProviderStore: deps.resolveWebSearchProviderStore(c), }); const session = await deps.sessions.create({ tenant_id: requestContext.tenant_id, @@ -452,6 +457,7 @@ export function createSessionsRouter(deps: SessionsRouterDeps) { mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + webSearchProviderStore: deps.resolveWebSearchProviderStore(c), }); } try { diff --git a/packages/trueforge/src/apis/settings.ts b/packages/trueforge/src/apis/settings.ts index 783da8d36..ab77761d7 100644 --- a/packages/trueforge/src/apis/settings.ts +++ b/packages/trueforge/src/apis/settings.ts @@ -11,11 +11,13 @@ import type { IMcpServerWithAuthStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import type { WithTransaction } from '../db/transaction'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import type { IOAuthTokenStore } from '../mcp/auth/types'; import { createSettingsMcpServersRouter } from './mcpServers'; import { createModelProvidersRouter } from './modelProviders'; import { createSandboxProvidersRouter } from './sandboxProviders'; import { createSkillsRouter, type ResolveSkillStore } from './skills'; +import { createWebSearchProvidersRouter } from './webSearchProviders'; export interface SettingsRouterDeps { resolveModelProviderStore: (c: Context) => IModelProviderStore; @@ -23,6 +25,7 @@ export interface SettingsRouterDeps { tokenStore: IOAuthTokenStore; resolveSkillStore: ResolveSkillStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; withTransaction: WithTransaction; logger: Logger; resolveRequestContext: ResolveRequestContext; @@ -65,5 +68,13 @@ export function createSettingsRouter(deps: SettingsRouterDeps; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; logger: Logger; resolveRequestContext: ResolveRequestContext; authorizer: Authorizer; @@ -135,6 +137,7 @@ export type BeginTurnExecutionDeps = Pick; }; @@ -405,13 +408,17 @@ export async function beginTurnExecution(params: { const abortController = new AbortController(); const tenant_id = session.tenant_id; + const webSearchProvider = await resolveWebSearchProvider({ + tenant_id, + store: deps.webSearchProviderStore, + }); const resolver = createTurnResolver({ mcpServerStore: deps.mcpServerStore, skillStore: deps.skillStore, sandboxProviderStore: deps.sandboxProviderStore, agentStore: deps.agentStore, modelProviderStore: deps.modelProviderStore, - webSearchProvider: resolveWebSearchProvider(), + webSearchProvider, logger: deps.logger, signal: abortController.signal, userRef, @@ -797,6 +804,7 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { skillStore: deps.resolveSkillStore(c), agentStore: deps.resolveAgentStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + webSearchProviderStore: deps.resolveWebSearchProviderStore(c), }, }; diff --git a/packages/trueforge/src/apis/webSearchProviders.ts b/packages/trueforge/src/apis/webSearchProviders.ts new file mode 100644 index 000000000..788aa71ca --- /dev/null +++ b/packages/trueforge/src/apis/webSearchProviders.ts @@ -0,0 +1,122 @@ +import { OpenAPIHono, type RouteHandler } from '@hono/zod-openapi'; +import type { Context } from 'hono'; +import type { ResolveRequestContext } from '../auth/identity'; +import type { WithTransaction } from '../db/transaction'; +import { + WebSearchProviderNameConflictError, + type IWebSearchProviderStore, + type WebSearchProviderRecord, +} from '../db/webSearchProviderStore'; +import { + createWebSearchProviderRoute, + listWebSearchProvidersRoute, + putWebSearchProviderRoute, +} from '../routes/webSearchProviderRoutes'; +import { + webSearchProviderName, + type ConfiguredWebSearchProvider, + type CreateWebSearchProviderRequest, + type UpdateWebSearchProviderRequest, + type WebSearchProviderManifest, +} from '../schemas/webSearchProvider'; +import { MissingStoredSecretError, resolveStoredSecretValue, toRedactedSecretValue } from '../utils/secretRedaction'; + +export interface WebSearchProvidersRouterDeps { + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; + withTransaction: WithTransaction; + resolveRequestContext: ResolveRequestContext; +} + +function redactWebSearchProvider(manifest: WebSearchProviderManifest): WebSearchProviderManifest { + return { + ...manifest, + auth: { api_key: toRedactedSecretValue(manifest.auth.api_key) }, + }; +} + +function resolveWebSearchProviderManifestForWrite({ + incoming, + existing, +}: { + incoming: WebSearchProviderManifest; + existing: WebSearchProviderManifest | undefined; +}): WebSearchProviderManifest { + return { + ...incoming, + auth: { + api_key: resolveStoredSecretValue({ + incoming: incoming.auth.api_key, + existing: existing?.auth.api_key, + }), + }, + }; +} + +function toWireProvider(record: WebSearchProviderRecord): ConfiguredWebSearchProvider { + return { + name: record.name, + manifest: redactWebSearchProvider(record.manifest), + }; +} + +export function createWebSearchProvidersRouter(deps: WebSearchProvidersRouterDeps) { + const listHandler: RouteHandler = async c => { + const requestContext = deps.resolveRequestContext(c); + const records = await deps.resolveWebSearchProviderStore(c).listProviders({ tenant_id: requestContext.tenant_id }); + return c.json({ data: records.map(toWireProvider) }, 200); + }; + + const createHandler: RouteHandler = async c => { + const body: CreateWebSearchProviderRequest = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + const provider = body.manifest; + const name = webSearchProviderName(provider); + try { + const manifest = resolveWebSearchProviderManifestForWrite({ incoming: provider, existing: undefined }); + const record = await deps.resolveWebSearchProviderStore(c).createProvider({ + tenant_id: requestContext.tenant_id, + name, + manifest, + }); + return c.json({ data: toWireProvider(record) }, 201); + } catch (error) { + if (error instanceof MissingStoredSecretError) { + return c.json({ error: { message: 'API key is required' } }, 400); + } + if (error instanceof WebSearchProviderNameConflictError) { + return c.json({ error: { message: error.message } }, 409); + } + throw error; + } + }; + + const putHandler: RouteHandler = async c => { + const store = deps.resolveWebSearchProviderStore(c); + const body: UpdateWebSearchProviderRequest = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + const provider = body.manifest; + const name = webSearchProviderName(provider); + try { + const record = await deps.withTransaction(async transaction => { + const existing = await store.getProviderForUpdate({ tenant_id: requestContext.tenant_id, name }, transaction); + const manifest = resolveWebSearchProviderManifestForWrite({ + incoming: provider, + existing: existing?.manifest, + }); + return store.upsertProvider({ tenant_id: requestContext.tenant_id, name, manifest }, transaction); + }); + return c.json({ data: toWireProvider(record) }, 200); + } catch (error) { + if (error instanceof MissingStoredSecretError) { + return c.json({ error: { message: 'API key is required' } }, 400); + } + throw error; + } + }; + + const router = new OpenAPIHono(); + router.openapi(listWebSearchProvidersRoute, listHandler); + router.openapi(createWebSearchProviderRoute, createHandler); + router.openapi(putWebSearchProviderRoute, putHandler); + return router; +} diff --git a/packages/trueforge/src/app.ts b/packages/trueforge/src/app.ts index a8c9ef36e..dd7920f13 100644 --- a/packages/trueforge/src/app.ts +++ b/packages/trueforge/src/app.ts @@ -38,6 +38,7 @@ import type { McpCatalog } from './catalog/McpCatalog'; import type { ModelCatalog } from './catalog/ModelCatalog'; import type { SandboxCatalog } from './catalog/SandboxCatalog'; import type { SkillCatalog } from './catalog/SkillCatalog'; +import type { WebSearchCatalog } from './catalog/WebSearchCatalog'; import configuration, { getPublicUiBasePath, getTrueForgeAuthMode, TrueForgeAuthMode } from './config'; import type { AgentRecord, IAgentStore } from './db/agentStore'; import type { IMcpServerWithAuthStore } from './db/mcpServerStore'; @@ -47,6 +48,7 @@ import type { IScheduleStore } from './db/scheduleStore'; import type { ISessionMetricsStore } from './db/sessionMetricsStore'; import type { ISkillStore } from './db/skillStore'; import type { WithTransaction } from './db/transaction'; +import type { IWebSearchProviderStore } from './db/webSearchProviderStore'; import { createClientCertificateMiddleware } from './http/tls'; import type { IOAuthTokenStore } from './mcp/auth/types'; import { PACKAGE_VERSION } from './packageVersion'; @@ -177,6 +179,7 @@ export interface ServerDeps { mcpCatalog: McpCatalog; skillCatalog: SkillCatalog; sandboxCatalog: SandboxCatalog; + webSearchCatalog: WebSearchCatalog; /** Per-request store: DB singleton, or a token-bound TrueFoundry store in TrueFoundry mode. */ resolveModelProviderStore: (c: Context, runAsAgent?: AgentRecord) => IModelProviderStore; /** @@ -193,6 +196,7 @@ export interface ServerDeps { * (`TRUEFOUNDRY_SANDBOX_*` + static SETTINGS JSON). */ resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; /** Per-request store: DB git skills, or TrueFoundry registry catalog in TrueFoundry mode. */ resolveSkillStore: ResolveSkillStore; withTransaction: WithTransaction; @@ -236,6 +240,7 @@ export function createServerApp(deps: ServerDeps) { resolveModelProviderStore: deps.resolveModelProviderStore, resolveMcpServerStore: deps.resolveMcpServerStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, activeTurns: deps.activeTurns, turnSkillsResolverStore: deps.turnSkillsResolverStore, }; @@ -263,6 +268,7 @@ export function createServerApp(deps: ServerDeps) { withAuth( createCapabilitiesRouter({ resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, withTransaction: deps.withTransaction, logger: deps.logger, resolveRequestContext, @@ -289,6 +295,7 @@ export function createServerApp(deps: ServerDeps) { mcpCatalog: deps.mcpCatalog, skillCatalog: deps.skillCatalog, sandboxCatalog: deps.sandboxCatalog, + webSearchCatalog: deps.webSearchCatalog, }), authMiddleware, ), @@ -336,6 +343,7 @@ export function createServerApp(deps: ServerDeps) { resolveMcpServerStore: deps.resolveMcpServerStore, resolveSkillStore: deps.resolveSkillStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, withTransaction: deps.withTransaction, resolveRequestContext, authorizer: deps.authorizer, @@ -369,6 +377,7 @@ export function createServerApp(deps: ServerDeps) { tokenStore: deps.tokenStore, resolveSkillStore: deps.resolveSkillStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, withTransaction: deps.withTransaction, logger: deps.logger, resolveRequestContext, @@ -396,6 +405,7 @@ export function createServerApp(deps: ServerDeps) { resolveSkillStore: deps.resolveSkillStore, resolveAgentStore: deps.resolveAgentStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, resolveRequestContext, authorizer: deps.authorizer, }), @@ -439,6 +449,7 @@ export function createServerApp(deps: ServerDeps) { resolveSkillStore: deps.resolveSkillStore, resolveAgentStore: deps.resolveAgentStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, redis: deps.redis, requestReplyRouter: deps.requestReplyRouter, resolveRequestContext, @@ -461,6 +472,7 @@ export function createServerApp(deps: ServerDeps) { resolveAgentStore: deps.resolveAgentStore, eventSubscriptions: deps.eventSubscriptions, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveWebSearchProviderStore: deps.resolveWebSearchProviderStore, logger: deps.logger, resolveRequestContext, authorizer: deps.authorizer, diff --git a/packages/trueforge/src/catalog/WebSearchCatalog.ts b/packages/trueforge/src/catalog/WebSearchCatalog.ts new file mode 100644 index 000000000..400acbad4 --- /dev/null +++ b/packages/trueforge/src/catalog/WebSearchCatalog.ts @@ -0,0 +1,25 @@ +import configuration from '../config'; +import { WebSearchCatalogFileSchema, type CatalogWebSearchProvider } from '../schemas/webSearchCatalog'; +import { loadYamlAtPath, parseYamlString } from './loadYaml'; +import { shippedWebSearchCatalogYaml } from './webSearchCatalog.gen'; + +export class WebSearchCatalog { + private readonly providers: readonly CatalogWebSearchProvider[]; + + constructor(providers: readonly CatalogWebSearchProvider[]) { + this.providers = providers; + } + + static load(): WebSearchCatalog { + if (configuration.WEB_SEARCH_CATALOG_PATH) { + const file = loadYamlAtPath(configuration.WEB_SEARCH_CATALOG_PATH, WebSearchCatalogFileSchema); + return new WebSearchCatalog(file.providers); + } + const file = parseYamlString(shippedWebSearchCatalogYaml, WebSearchCatalogFileSchema, 'shipped web-search-catalog'); + return new WebSearchCatalog(file.providers); + } + + list(): readonly CatalogWebSearchProvider[] { + return this.providers; + } +} diff --git a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts new file mode 100644 index 000000000..04d5110ad --- /dev/null +++ b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts @@ -0,0 +1,2 @@ +// AUTO-GENERATED by scripts/generate-catalog.mjs — do not edit. +export const shippedWebSearchCatalogYaml = "providers:\n - type: parallel\n mode: turbo\n" as const; diff --git a/packages/trueforge/src/config.ts b/packages/trueforge/src/config.ts index 576379821..8a0e2b5b8 100644 --- a/packages/trueforge/src/config.ts +++ b/packages/trueforge/src/config.ts @@ -564,6 +564,12 @@ export interface SharedServerConfiguration { * time is used. Env: `SANDBOX_CATALOG_PATH`. */ SANDBOX_CATALOG_PATH: string | undefined; + /** + * Optional override for the web-search catalog YAML (discovery presets for + * GET /catalogs/web-search-providers). When unset, the catalog inlined at build + * time is used. Env: `WEB_SEARCH_CATALOG_PATH`. + */ + WEB_SEARCH_CATALOG_PATH: string | undefined; /** * Frontend build served alongside the API; a missing directory leaves the server API-only. * Env: `FRONTEND_DIR`. Default: packaged `dist/_frontend` (npx tarball) or @@ -857,6 +863,7 @@ const shared: SharedServerConfiguration = { MCP_CATALOG_PATH: resolveOptionalPathEnv('MCP_CATALOG_PATH'), SKILL_CATALOG_PATH: resolveOptionalPathEnv('SKILL_CATALOG_PATH'), SANDBOX_CATALOG_PATH: resolveOptionalPathEnv('SANDBOX_CATALOG_PATH'), + WEB_SEARCH_CATALOG_PATH: resolveOptionalPathEnv('WEB_SEARCH_CATALOG_PATH'), FRONTEND_DIR: resolveFrontendDir(), MCP_REQUEST_TIMEOUT_MS: parsePositiveInt({ diff --git a/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts b/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts new file mode 100644 index 000000000..f6e26efbf --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts @@ -0,0 +1,19 @@ +import { sql, type Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema + .createTable('web_search_provider') + .addColumn('tenant_id', 'text', col => col.notNull()) + .addColumn('name', 'text', col => col.notNull()) + .addColumn('manifest', 'jsonb', col => col.notNull()) + .addColumn('created_at', 'timestamptz', col => col.notNull()) + .addColumn('updated_at', 'timestamptz', col => col.notNull()) + .addPrimaryKeyConstraint('web_search_provider_pkey', ['tenant_id', 'name']) + .execute(); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.dropTable('web_search_provider').ifExists().cascade().execute(); +} diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 41d78f462..584e3a64c 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -32,6 +32,7 @@ import type { } from '../../schemas/sandboxProvider'; import type { ScheduleManifest, ScheduleRunStatus, ScheduleStatus } from '../../schemas/schedule'; import type { SkillManifest } from '../../schemas/skill'; +import type { WebSearchProviderManifest } from '../../schemas/webSearchProvider'; import type { OAuthClient, OAuthPendingAuthorizationData, OAuthServer, OAuthToken } from '../mcpServerStore'; /** @@ -341,6 +342,14 @@ export interface ModelProviderTable { updated_at: Date; } +export interface WebSearchProviderTable { + tenant_id: string; + name: string; + manifest: JSONColumnType; + created_at: Date; + updated_at: Date; +} + /** * Configured skills — mirrors the Postgres `skill` table. * PRIMARY KEY (tenant_id, name) @@ -523,6 +532,7 @@ export interface Database { thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; + web_search_provider: WebSearchProviderTable; skill: SkillTable; sandbox_provider: SandboxProviderTable; agent: AgentTable; diff --git a/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts b/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts new file mode 100644 index 000000000..cf1b30494 --- /dev/null +++ b/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts @@ -0,0 +1,128 @@ +import type { Kysely, Selectable, Transaction } from 'kysely'; +import { + WebSearchProviderNameConflictError, + type CreateWebSearchProviderInput, + type GetWebSearchProviderForUpdateInput, + type GetWebSearchProviderInput, + type IWebSearchProviderStore, + type ListWebSearchProvidersInput, + type UpsertWebSearchProviderInput, + type WebSearchProviderRecord, +} from '../../webSearchProviderStore'; +import { isUniqueViolation } from '../client'; +import { json, now } from '../sqlExpressions'; +import type { Database, WebSearchProviderTable } from '../types'; + +function toRecord(row: Selectable): WebSearchProviderRecord { + return { + tenant_id: row.tenant_id, + name: row.name, + manifest: row.manifest, + created_at: row.created_at.toISOString(), + updated_at: row.updated_at.toISOString(), + }; +} + +export class PostgresWebSearchProviderStore implements IWebSearchProviderStore> { + readonly #db: Kysely; + + constructor(db: Kysely) { + this.#db = db; + } + + async listProviders( + input: ListWebSearchProvidersInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('web_search_provider') + .selectAll() + .where('tenant_id', '=', input.tenant_id) + .orderBy('name') + .execute(); + return rows.map(toRecord); + } + + async getProvider( + input: GetWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const row = await db + .selectFrom('web_search_provider') + .selectAll() + .where('tenant_id', '=', input.tenant_id) + .where('name', '=', input.name) + .executeTakeFirst(); + return row ? toRecord(row) : undefined; + } + + async getProviderForUpdate( + input: GetWebSearchProviderForUpdateInput, + transaction: Transaction, + ): Promise { + const row = await transaction + .selectFrom('web_search_provider') + .selectAll() + .where('tenant_id', '=', input.tenant_id) + .where('name', '=', input.name) + .forUpdate() + .executeTakeFirst(); + return row ? toRecord(row) : undefined; + } + + async createProvider( + input: CreateWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + try { + const row = await db + .insertInto('web_search_provider') + .values({ + tenant_id: input.tenant_id, + name: input.name, + manifest: json(input.manifest), + created_at: now(), + updated_at: now(), + }) + .returningAll() + .executeTakeFirstOrThrow(); + return toRecord(row); + } catch (error) { + if (isUniqueViolation(error)) { + throw new WebSearchProviderNameConflictError( + { tenant_id: input.tenant_id, name: input.name }, + { cause: error }, + ); + } + throw error; + } + } + + async upsertProvider( + input: UpsertWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const row = await db + .insertInto('web_search_provider') + .values({ + tenant_id: input.tenant_id, + name: input.name, + manifest: json(input.manifest), + created_at: now(), + updated_at: now(), + }) + .onConflict(oc => + oc.columns(['tenant_id', 'name']).doUpdateSet({ + manifest: json(input.manifest), + updated_at: now(), + }), + ) + .returningAll() + .executeTakeFirstOrThrow(); + return toRecord(row); + } +} diff --git a/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts b/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts new file mode 100644 index 000000000..8c9075a9a --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts @@ -0,0 +1,22 @@ +import { sql, type Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE web_search_provider ( + tenant_id TEXT NOT NULL, + name TEXT NOT NULL, + manifest BLOB NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (tenant_id, name) + ) STRICT + `.execute(trx); + }); +} + +export async function down(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql`DROP TABLE IF EXISTS web_search_provider`.execute(trx); + }); +} diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 9c2b62303..1a1534811 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -35,6 +35,7 @@ import type { } from '../../schemas/sandboxProvider'; import type { ScheduleManifest, ScheduleRunStatus, ScheduleStatus } from '../../schemas/schedule'; import type { SkillManifest } from '../../schemas/skill'; +import type { WebSearchProviderManifest } from '../../schemas/webSearchProvider'; import type { OAuthClient, OAuthPendingAuthorizationData, OAuthServer, OAuthToken } from '../mcpServerStore'; /** @@ -187,6 +188,14 @@ export interface ModelProviderTable { updated_at: string; } +export interface WebSearchProviderTable { + tenant_id: string; + name: string; + manifest: JsonbColumn; + created_at: string; + updated_at: string; +} + /** * Configured skills — mirrors the Postgres `skill` table. * PRIMARY KEY (tenant_id, name) @@ -343,6 +352,7 @@ export interface Database { thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; + web_search_provider: WebSearchProviderTable; skill: SkillTable; sandbox_provider: SandboxProviderTable; agent: AgentTable; diff --git a/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts b/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts new file mode 100644 index 000000000..af3f5f3ce --- /dev/null +++ b/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts @@ -0,0 +1,127 @@ +import type { ExpressionBuilder, Kysely, Transaction } from 'kysely'; +import type { WebSearchProviderManifest } from '../../../schemas/webSearchProvider'; +import { + WebSearchProviderNameConflictError, + type CreateWebSearchProviderInput, + type GetWebSearchProviderForUpdateInput, + type GetWebSearchProviderInput, + type IWebSearchProviderStore, + type ListWebSearchProvidersInput, + type UpsertWebSearchProviderInput, + type WebSearchProviderRecord, +} from '../../webSearchProviderStore'; +import { isUniqueViolation } from '../client'; +import { jsonbBind, jsonText, nowIso } from '../sqlExpressions'; +import type { Database } from '../types'; + +/** Projects JSONB manifest via JSON_RESULT_COLUMNS. */ +function recordColumns(eb: ExpressionBuilder) { + return [ + 'tenant_id' as const, + 'name' as const, + jsonText(eb.ref('manifest')).as('manifest'), + 'created_at' as const, + 'updated_at' as const, + ]; +} + +export class SqliteWebSearchProviderStore implements IWebSearchProviderStore> { + readonly #db: Kysely; + + constructor(db: Kysely) { + this.#db = db; + } + + async listProviders( + input: ListWebSearchProvidersInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + return await db + .selectFrom('web_search_provider') + .select(recordColumns) + .where('tenant_id', '=', input.tenant_id) + .orderBy('name') + .execute(); + } + + async getProvider( + input: GetWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + return await db + .selectFrom('web_search_provider') + .select(recordColumns) + .where('tenant_id', '=', input.tenant_id) + .where('name', '=', input.name) + .executeTakeFirst(); + } + + /** SQLite has no FOR UPDATE; write txn serializes secret keep/rotate. */ + async getProviderForUpdate( + input: GetWebSearchProviderForUpdateInput, + transaction: Transaction, + ): Promise { + return await transaction + .selectFrom('web_search_provider') + .select(recordColumns) + .where('tenant_id', '=', input.tenant_id) + .where('name', '=', input.name) + .executeTakeFirst(); + } + + async createProvider( + input: CreateWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const timestamp = nowIso(); + try { + return await db + .insertInto('web_search_provider') + .values({ + tenant_id: input.tenant_id, + name: input.name, + manifest: jsonbBind(input.manifest), + created_at: timestamp, + updated_at: timestamp, + }) + .returning(recordColumns) + .executeTakeFirstOrThrow(); + } catch (error) { + if (isUniqueViolation(error)) { + throw new WebSearchProviderNameConflictError( + { tenant_id: input.tenant_id, name: input.name }, + { cause: error }, + ); + } + throw error; + } + } + + async upsertProvider( + input: UpsertWebSearchProviderInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const timestamp = nowIso(); + return await db + .insertInto('web_search_provider') + .values({ + tenant_id: input.tenant_id, + name: input.name, + manifest: jsonbBind(input.manifest), + created_at: timestamp, + updated_at: timestamp, + }) + .onConflict(oc => + oc.columns(['tenant_id', 'name']).doUpdateSet({ + manifest: jsonbBind(input.manifest), + updated_at: timestamp, + }), + ) + .returning(recordColumns) + .executeTakeFirstOrThrow(); + } +} diff --git a/packages/trueforge/src/db/webSearchProviderStore.ts b/packages/trueforge/src/db/webSearchProviderStore.ts new file mode 100644 index 000000000..f73c0dfef --- /dev/null +++ b/packages/trueforge/src/db/webSearchProviderStore.ts @@ -0,0 +1,59 @@ +import type { ResourceName } from '../schemas/common'; +import type { WebSearchProviderManifest } from '../schemas/webSearchProvider'; + +export interface WebSearchProviderRecord { + tenant_id: string; + name: ResourceName; + manifest: WebSearchProviderManifest; + created_at: string; + updated_at: string; +} + +export interface ListWebSearchProvidersInput { + tenant_id: string; +} + +export interface GetWebSearchProviderInput { + tenant_id: string; + name: string; +} + +export interface GetWebSearchProviderForUpdateInput { + tenant_id: string; + name: string; +} + +export interface CreateWebSearchProviderInput { + tenant_id: string; + name: ResourceName; + manifest: WebSearchProviderManifest; +} + +export type UpsertWebSearchProviderInput = CreateWebSearchProviderInput; + +export class WebSearchProviderNameConflictError extends Error { + readonly tenant_id: string; + readonly provider_name: string; + + constructor({ tenant_id, name }: { tenant_id: string; name: string }, options?: ErrorOptions) { + super(`Web search provider name already exists: ${name}`, options); + this.name = 'WebSearchProviderNameConflictError'; + this.tenant_id = tenant_id; + this.provider_name = name; + } +} + +export interface IWebSearchProviderStore { + listProviders(input: ListWebSearchProvidersInput, transaction?: TTransaction): Promise; + getProvider( + input: GetWebSearchProviderInput, + transaction?: TTransaction, + ): Promise; + /** Row lock for secret keep/rotate; Postgres FOR UPDATE, SQLite write txn. */ + getProviderForUpdate( + input: GetWebSearchProviderForUpdateInput, + transaction: TTransaction, + ): Promise; + createProvider(input: CreateWebSearchProviderInput, transaction?: TTransaction): Promise; + upsertProvider(input: UpsertWebSearchProviderInput, transaction?: TTransaction): Promise; +} diff --git a/packages/trueforge/src/main.ts b/packages/trueforge/src/main.ts index e4d617654..a3bb075af 100644 --- a/packages/trueforge/src/main.ts +++ b/packages/trueforge/src/main.ts @@ -70,6 +70,7 @@ import { McpCatalog } from './catalog/McpCatalog'; import { ModelCatalog } from './catalog/ModelCatalog'; import { SandboxCatalog } from './catalog/SandboxCatalog'; import { SkillCatalog } from './catalog/SkillCatalog'; +import { WebSearchCatalog } from './catalog/WebSearchCatalog'; import { type DistributedServerConfiguration } from './config'; import { createController } from './controller'; import type { AgentRecord, IAgentStore } from './db/agentStore'; @@ -84,6 +85,7 @@ import type { ISessionMetricsStore } from './db/sessionMetricsStore'; import type { ISkillStore } from './db/skillStore'; import type { Database as SqliteDatabase } from './db/sqlite/types'; import type { WithTransaction } from './db/transaction'; +import type { IWebSearchProviderStore } from './db/webSearchProviderStore'; import { mountFrontend } from './frontend'; import { serverTlsServeOptions } from './http/tls'; import { createServerLogger, shouldColorize } from './logger'; @@ -107,6 +109,7 @@ import { TrueFoundryModelProviderStore } from './truefoundry/TrueFoundryModelPro import { TrueFoundrySandboxProviderStore } from './truefoundry/TrueFoundrySandboxProviderStore'; import { TrueFoundryServiceFoundryServerClient } from './truefoundry/TrueFoundryServiceFoundryServerClient'; import { TrueFoundryAdminSkillStore, TrueFoundrySkillStore } from './truefoundry/TrueFoundrySkillStore'; +import { TrueFoundryWebSearchProviderStore } from './truefoundry/TrueFoundryWebSearchProviderStore'; /** Persistence + optional Redis wired for the selected topology. */ interface ServerPersistence { @@ -123,6 +126,7 @@ interface ServerPersistence { perServerHeaders?: PerServerMcpHeaders, ) => IMcpServerWithAuthStore; resolveSandboxProviderStore: (rc: RequestContext) => ISandboxProviderStore; + resolveWebSearchProviderStore: (rc: RequestContext) => IWebSearchProviderStore; /** Per-request store: DB git skills, or TrueFoundry registry catalog in TrueFoundry mode. */ resolveSkillStore: (rc: RequestContext) => ISkillStore; resolveAgentStore: (rc: RequestContext) => IAgentStore; @@ -279,6 +283,16 @@ function buildResolveSandboxProviderStore(options: { return () => persistenceStore; } +function buildResolveWebSearchProviderStore(options: { + persistenceStore: IWebSearchProviderStore; +}): (rc: RequestContext) => IWebSearchProviderStore { + const { persistenceStore } = options; + if (isTrueFoundryModeEnabled(configuration)) { + return () => new TrueFoundryWebSearchProviderStore(); + } + return () => persistenceStore; +} + /** SQLite stores; Redis unused (executor peering disabled). */ async function createStandalonePersistence(options: { sqlitePath: string; @@ -297,6 +311,7 @@ async function createStandalonePersistence(options: { import('./db/sqlite/token-store/SqliteOAuthTokenStore'), import('./db/sqlite/skill-store/SqliteSkillStore'), import('./db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'), + import('./db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'), import('./db/sqlite/agent-store/SqliteAgentStore'), import('./db/sqlite/schedule-store/SqliteScheduleStore'), ]), @@ -309,6 +324,7 @@ async function createStandalonePersistence(options: { { SqliteOAuthTokenStore }, { SqliteSkillStore }, { SqliteSandboxProviderStore }, + { SqliteWebSearchProviderStore }, { SqliteAgentStore }, { SqliteScheduleStore }, ] = sqliteStores; @@ -327,6 +343,7 @@ async function createStandalonePersistence(options: { clientName: configuration.MCP_DCR_OAUTH_CLIENT_NAME, }); const sandboxProviderStore = new SqliteSandboxProviderStore(db); + const webSearchProviderStore = new SqliteWebSearchProviderStore(db); const skillStore = new SqliteSkillStore(db); return { withTransaction: callback => db.transaction().execute(callback), @@ -338,6 +355,7 @@ async function createStandalonePersistence(options: { resolveModelProviderStore: () => modelProviderStore, resolveMcpServerStore: () => mcpServerStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveImportAgentStore: () => agentStore, @@ -377,6 +395,7 @@ async function createDistributedPersistence(options: { import('./db/postgres/token-store/PostgresOAuthTokenStore'), import('./db/postgres/skill-store/PostgresSkillStore'), import('./db/postgres/sandbox-provider-store/PostgresSandboxProviderStore'), + import('./db/postgres/web-search-provider-store/PostgresWebSearchProviderStore'), import('./db/postgres/agent-store/PostgresAgentStore'), import('./db/postgres/schedule-store/PostgresScheduleStore'), ]), @@ -389,6 +408,7 @@ async function createDistributedPersistence(options: { { PostgresOAuthTokenStore }, { PostgresSkillStore }, { PostgresSandboxProviderStore }, + { PostgresWebSearchProviderStore }, { PostgresAgentStore }, { PostgresScheduleStore }, ] = postgresStores; @@ -413,6 +433,7 @@ async function createDistributedPersistence(options: { clientName: configuration.MCP_DCR_OAUTH_CLIENT_NAME, }); const sandboxProviderStore = new PostgresSandboxProviderStore(db); + const webSearchProviderStore = new PostgresWebSearchProviderStore(db); const skillStore = new PostgresSkillStore(db); const agentStore = new PostgresAgentStore(db); const turnSkillsResolverStore = buildTurnSkillsResolverStore({ @@ -457,6 +478,9 @@ async function createDistributedPersistence(options: { const resolveSandboxProviderStore = buildResolveSandboxProviderStore({ persistenceStore: sandboxProviderStore, }); + const resolveWebSearchProviderStore = buildResolveWebSearchProviderStore({ + persistenceStore: webSearchProviderStore, + }); const resolveSkillStore = buildResolveSkillStore({ persistenceStore: skillStore, client: serviceFoundryClient, @@ -471,6 +495,7 @@ async function createDistributedPersistence(options: { resolveModelProviderStore, resolveMcpServerStore, resolveSandboxProviderStore, + resolveWebSearchProviderStore, resolveSkillStore, resolveAgentStore, resolveImportAgentStore, @@ -566,6 +591,8 @@ async function createServerRuntime(persistence: ServerPersistence< }; const resolveAgentStore = (c: Context) => persistence.resolveAgentStore(resolveRequestContext(c)); const resolveSandboxProviderStore = (c: Context) => persistence.resolveSandboxProviderStore(resolveRequestContext(c)); + const resolveWebSearchProviderStore = (c: Context) => + persistence.resolveWebSearchProviderStore(resolveRequestContext(c)); const resolveSkillStore = (c: Context) => { const store = persistence.resolveSkillStore(resolveRequestContext(c)); if (!isTrueFoundryModeEnabled(configuration)) { @@ -582,11 +609,13 @@ async function createServerRuntime(persistence: ServerPersistence< mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), resolveModelProviderStore, resolveMcpServerStore, resolveAgentStore, resolveImportAgentStore, resolveSandboxProviderStore, + resolveWebSearchProviderStore, resolveSkillStore, withTransaction, tokenStore, diff --git a/packages/trueforge/src/routes/catalogRoutes.ts b/packages/trueforge/src/routes/catalogRoutes.ts index 3eb95d688..0111ca1c3 100644 --- a/packages/trueforge/src/routes/catalogRoutes.ts +++ b/packages/trueforge/src/routes/catalogRoutes.ts @@ -8,6 +8,7 @@ import { GetMcpServerCatalogResponseSchema } from '../schemas/mcpCatalog'; import { GetModelProviderCatalogResponseSchema } from '../schemas/modelCatalog'; import { GetSandboxProviderCatalogResponseSchema } from '../schemas/sandboxCatalog'; import { GetSkillCatalogResponseSchema } from '../schemas/skillCatalog'; +import { GetWebSearchProviderCatalogResponseSchema } from '../schemas/webSearchCatalog'; import { OpenApiTag } from './openapiTags'; export const listModelProviderCatalogRoute = createRoute({ @@ -92,3 +93,24 @@ export const listSandboxProviderCatalogRoute = createRoute({ }, }, }); + +export const listWebSearchProviderCatalogRoute = createRoute({ + method: 'get', + path: '/web-search-providers', + tags: [OpenApiTag.WEB_SEARCH], + summary: 'Get the web-search provider catalog', + description: + 'Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.', + 'x-fern-sdk-group-name': ['catalogs', 'webSearchProviders'], + 'x-fern-sdk-method-name': 'list', + responses: { + 200: { + content: { 'application/json': { schema: GetWebSearchProviderCatalogResponseSchema } }, + description: 'Shipped web-search-provider presets.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Not authenticated.', + }, + }, +}); diff --git a/packages/trueforge/src/routes/openapiTags.ts b/packages/trueforge/src/routes/openapiTags.ts index 1082e870b..a19c089e0 100644 --- a/packages/trueforge/src/routes/openapiTags.ts +++ b/packages/trueforge/src/routes/openapiTags.ts @@ -7,6 +7,7 @@ export enum OpenApiTag { MCP_SERVERS = 'MCP Servers', SKILLS = 'Skills', SANDBOXES = 'Sandboxes', + WEB_SEARCH = 'Web Search', AGENTS = 'Agents', SCHEDULES = 'Schedules', AGENT_SESSIONS = 'Agent Sessions', @@ -21,6 +22,7 @@ export const OPENAPI_DOCUMENT_TAGS: { name: OpenApiTag }[] = [ { name: OpenApiTag.MCP_SERVERS }, { name: OpenApiTag.SKILLS }, { name: OpenApiTag.SANDBOXES }, + { name: OpenApiTag.WEB_SEARCH }, { name: OpenApiTag.AGENTS }, { name: OpenApiTag.SCHEDULES }, { name: OpenApiTag.AGENT_SESSIONS }, diff --git a/packages/trueforge/src/routes/webSearchProviderRoutes.ts b/packages/trueforge/src/routes/webSearchProviderRoutes.ts new file mode 100644 index 000000000..32a3558d7 --- /dev/null +++ b/packages/trueforge/src/routes/webSearchProviderRoutes.ts @@ -0,0 +1,101 @@ +import { createRoute } from '@hono/zod-openapi'; +import { RequestErrorResponseSchema } from '../schemas/errors'; +import { + CreateWebSearchProviderRequestSchema, + GetWebSearchProviderResponseSchema, + ListWebSearchProvidersResponseSchema, + UpdateWebSearchProviderRequestSchema, +} from '../schemas/webSearchProvider'; +import { OpenApiTag } from './openapiTags'; + +export const listWebSearchProvidersRoute = createRoute({ + method: 'get', + path: '/', + tags: [OpenApiTag.WEB_SEARCH], + summary: 'List configured web-search providers', + description: 'All configured providers with nested manifests. `auth.api_key` is redacted.', + 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], + 'x-fern-sdk-method-name': 'list', + responses: { + 200: { + content: { 'application/json': { schema: ListWebSearchProvidersResponseSchema } }, + description: 'All configured web-search providers', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'OIDC is configured and the request has no valid session cookie.', + }, + 403: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'OIDC is configured and the caller is authenticated but not an admin.', + }, + }, +}); + +export const createWebSearchProviderRoute = createRoute({ + method: 'post', + path: '/', + tags: [OpenApiTag.WEB_SEARCH], + summary: 'Create a web-search provider', + description: + 'Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). ' + + '`auth.api_key`: real value required; redacted with no stored secret returns 400.', + 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], + 'x-fern-sdk-method-name': 'create', + request: { + body: { + content: { 'application/json': { schema: CreateWebSearchProviderRequestSchema } }, + required: true, + }, + }, + responses: { + 201: { + content: { 'application/json': { schema: GetWebSearchProviderResponseSchema } }, + description: 'The created provider', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body, or redacted API key with no stored secret to keep.', + }, + 409: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'A web-search provider with this name already exists.', + }, + 424: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unsupported operation because web-search providers are managed by an external system', + }, + }, +}); + +export const putWebSearchProviderRoute = createRoute({ + method: 'put', + path: '/', + tags: [OpenApiTag.WEB_SEARCH], + summary: 'Create or replace a web-search provider', + description: + 'Create or replace a provider. Well-known types use `type` as `name` (one each). ' + + '`auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).', + 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], + 'x-fern-sdk-method-name': 'create_or_update', + request: { + body: { + content: { 'application/json': { schema: UpdateWebSearchProviderRequestSchema } }, + required: true, + }, + }, + responses: { + 200: { + content: { 'application/json': { schema: GetWebSearchProviderResponseSchema } }, + description: 'The saved provider', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body, or redacted API key with no stored secret to keep.', + }, + 424: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unsupported operation because web-search providers are managed by an external system', + }, + }, +}); diff --git a/packages/trueforge/src/runtime/sessionResources.ts b/packages/trueforge/src/runtime/sessionResources.ts index c1287be3e..61f76812a 100644 --- a/packages/trueforge/src/runtime/sessionResources.ts +++ b/packages/trueforge/src/runtime/sessionResources.ts @@ -19,11 +19,12 @@ import type { IMcpServerStore, IMcpServerWithAuthStore } from '../db/mcpServerSt import type { IModelProviderStore } from '../db/modelProviderStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import type { ISkillStore } from '../db/skillStore'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { LocalSandboxProvider } from '../sandbox/local/provider/LocalSandboxProvider'; import { getCachedLocalSandboxSupport, isLocalSandboxFallbackEnabled } from '../sandbox/localRuntime'; import { toSandboxProviderFromRecord } from '../sandbox/providerUtils'; import type { ReasoningEffort } from '../schemas/modelProvider'; -import { resolveWebSearchProvider } from '../websearch/providers'; +import { hasConfiguredWebSearchProvider } from '../websearch/providers'; export interface McpConnection { url: string; @@ -294,6 +295,7 @@ export async function validateAgentSpec({ mcpServerStore, skillStore, sandboxProviderStore, + webSearchProviderStore, }: { spec: AgentSpec; tenant_id: string; @@ -301,6 +303,7 @@ export async function validateAgentSpec({ mcpServerStore: IMcpServerStore; skillStore: ISkillStore; sandboxProviderStore: ISandboxProviderStore; + webSearchProviderStore: IWebSearchProviderStore; }): Promise { const resolved = await getModelDetails({ tenant_id, @@ -356,9 +359,12 @@ export async function validateAgentSpec({ } } - if (spec.config.web_search.enabled && resolveWebSearchProvider() === undefined) { - throw new HTTPException(422, { - message: 'web_search is enabled but no web-search provider is configured', - }); + if (spec.config.web_search.enabled) { + const hasProvider = await hasConfiguredWebSearchProvider({ tenant_id, store: webSearchProviderStore }); + if (!hasProvider) { + throw new HTTPException(422, { + message: 'web_search is enabled but no web-search provider is configured', + }); + } } } diff --git a/packages/trueforge/src/schemas/webSearchCatalog.ts b/packages/trueforge/src/schemas/webSearchCatalog.ts new file mode 100644 index 000000000..14f518c04 --- /dev/null +++ b/packages/trueforge/src/schemas/webSearchCatalog.ts @@ -0,0 +1,22 @@ +import { z } from '@hono/zod-openapi'; +import { ParallelWebSearchProviderSchema } from './webSearchProvider'; + +/** Single variant today (avoids one-member `oneOf` in OpenAPI). */ +export const CatalogWebSearchProviderSchema = ParallelWebSearchProviderSchema.omit({ auth: true }) + .strict() + .openapi('CatalogWebSearchProvider'); + +export const WebSearchCatalogFileSchema = z + .object({ + providers: z.array(CatalogWebSearchProviderSchema), + }) + .strict(); + +export const GetWebSearchProviderCatalogResponseSchema = z + .object({ + data: z.array(CatalogWebSearchProviderSchema), + }) + .openapi('GetWebSearchProviderCatalogResponse'); + +export type CatalogWebSearchProvider = z.infer; +export type WebSearchCatalogFile = z.infer; diff --git a/packages/trueforge/src/schemas/webSearchProvider.ts b/packages/trueforge/src/schemas/webSearchProvider.ts new file mode 100644 index 000000000..3e3024d05 --- /dev/null +++ b/packages/trueforge/src/schemas/webSearchProvider.ts @@ -0,0 +1,75 @@ +import { z } from '@hono/zod-openapi'; +import { NameSchema, type ResourceName } from './common'; + +export const ParallelWebSearchModeSchema = z + .enum(['turbo', 'fast', 'basic', 'advanced']) + .describe('Parallel Search mode preset.') + .openapi('ParallelWebSearchMode'); + +const ParallelWebSearchProviderAuthSchema = z + .object({ + api_key: z + .string() + .min(1) + .describe( + 'Parallel API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key.', + ), + }) + .strict() + .describe('Parallel authentication credentials.') + .openapi('ParallelWebSearchProviderAuth'); + +/** Single variant today (avoids one-member `oneOf` in OpenAPI). */ +export const ParallelWebSearchProviderSchema = z + .object({ + type: z.literal('parallel').describe('Parallel web-search provider.'), + auth: ParallelWebSearchProviderAuthSchema, + mode: ParallelWebSearchModeSchema, + }) + .strict(); + +export const WebSearchProviderManifestSchema = ParallelWebSearchProviderSchema.openapi('WebSearchProviderManifest'); + +export function webSearchProviderName(provider: WebSearchProviderManifest): ResourceName { + return provider.type; +} + +export const ConfiguredWebSearchProviderSchema = z + .object({ + name: NameSchema, + manifest: WebSearchProviderManifestSchema, + }) + .strict() + .openapi('ConfiguredWebSearchProvider'); + +export const CreateWebSearchProviderRequestSchema = z + .object({ + manifest: WebSearchProviderManifestSchema, + }) + .strict() + .openapi('CreateWebSearchProviderRequest'); + +export const UpdateWebSearchProviderRequestSchema = z + .object({ + manifest: WebSearchProviderManifestSchema, + }) + .strict() + .openapi('UpdateWebSearchProviderRequest'); + +export const GetWebSearchProviderResponseSchema = z + .object({ + data: ConfiguredWebSearchProviderSchema, + }) + .openapi('GetWebSearchProviderResponse'); + +export const ListWebSearchProvidersResponseSchema = z + .object({ + data: z.array(ConfiguredWebSearchProviderSchema), + }) + .openapi('ListWebSearchProvidersResponse'); + +export type ParallelWebSearchMode = z.infer; +export type WebSearchProviderManifest = z.infer; +export type ConfiguredWebSearchProvider = z.infer; +export type CreateWebSearchProviderRequest = z.infer; +export type UpdateWebSearchProviderRequest = z.infer; diff --git a/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts b/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts new file mode 100644 index 000000000..9b02ebe0e --- /dev/null +++ b/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts @@ -0,0 +1,76 @@ +import configuration, { isTrueFoundryModeEnabled } from '../config'; +import type { + CreateWebSearchProviderInput, + GetWebSearchProviderForUpdateInput, + GetWebSearchProviderInput, + IWebSearchProviderStore, + ListWebSearchProvidersInput, + UpsertWebSearchProviderInput, + WebSearchProviderRecord, +} from '../db/webSearchProviderStore'; +import { webSearchProviderName } from '../schemas/webSearchProvider'; +import { trueFoundryManaged } from './errors'; + +function synthesizeParallelRecord(tenantId: string): WebSearchProviderRecord | undefined { + if (!isTrueFoundryModeEnabled(configuration)) { + return undefined; + } + const env = configuration.TRUEFOUNDRY_WEB_SEARCH_PROVIDER; + if (!env) { + return undefined; + } + const manifest = { + type: 'parallel' as const, + auth: { api_key: env.api_key }, + mode: 'turbo' as const, + }; + const now = new Date().toISOString(); + return { + tenant_id: tenantId, + name: webSearchProviderName(manifest), + manifest, + created_at: now, + updated_at: now, + }; +} + +export class TrueFoundryWebSearchProviderStore implements IWebSearchProviderStore { + listProviders(input: ListWebSearchProvidersInput, transaction?: TTransaction): Promise { + void transaction; + const record = synthesizeParallelRecord(input.tenant_id); + return Promise.resolve(record ? [record] : []); + } + + getProvider( + input: GetWebSearchProviderInput, + transaction?: TTransaction, + ): Promise { + void transaction; + const record = synthesizeParallelRecord(input.tenant_id); + if (record?.name !== input.name) { + return Promise.resolve(undefined); + } + return Promise.resolve(record); + } + + getProviderForUpdate( + input: GetWebSearchProviderForUpdateInput, + transaction: TTransaction, + ): Promise { + void input; + void transaction; + return trueFoundryManaged(); + } + + createProvider(input: CreateWebSearchProviderInput, transaction?: TTransaction): Promise { + void input; + void transaction; + return trueFoundryManaged(); + } + + upsertProvider(input: UpsertWebSearchProviderInput, transaction?: TTransaction): Promise { + void input; + void transaction; + return trueFoundryManaged(); + } +} diff --git a/packages/trueforge/src/websearch/providers.ts b/packages/trueforge/src/websearch/providers.ts index 7f16efc28..8090d685e 100644 --- a/packages/trueforge/src/websearch/providers.ts +++ b/packages/trueforge/src/websearch/providers.ts @@ -1,14 +1,29 @@ import { ParallelWebSearchProvider, type IWebSearchProvider } from '@truefoundry/trueforge-core/core'; -import configuration, { isTrueFoundryModeEnabled } from '../config'; +import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; -/** Build the host web-search backend (TrueFoundry mode only), or `undefined` when unset. */ -export function resolveWebSearchProvider(): IWebSearchProvider | undefined { - if (!isTrueFoundryModeEnabled(configuration)) { - return undefined; - } - const env = configuration.TRUEFOUNDRY_WEB_SEARCH_PROVIDER; - if (!env) { +export async function hasConfiguredWebSearchProvider({ + tenant_id, + store, +}: { + tenant_id: string; + store: IWebSearchProviderStore; +}): Promise { + const providers = await store.listProviders({ tenant_id }); + return providers.length > 0; +} + +export async function resolveWebSearchProvider({ + tenant_id, + store, +}: { + tenant_id: string; + store: IWebSearchProviderStore; +}): Promise { + const providers = await store.listProviders({ tenant_id }); + const record = providers[0]; + if (!record) { return undefined; } - return new ParallelWebSearchProvider({ apiKey: env.api_key, mode: 'turbo' }); + const { manifest } = record; + return new ParallelWebSearchProvider({ apiKey: manifest.auth.api_key, mode: manifest.mode }); } diff --git a/packages/trueforge/tests/unit/apis/agents.test.ts b/packages/trueforge/tests/unit/apis/agents.test.ts index dbb83d6c6..a85368e0a 100644 --- a/packages/trueforge/tests/unit/apis/agents.test.ts +++ b/packages/trueforge/tests/unit/apis/agents.test.ts @@ -9,6 +9,7 @@ import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/Sq import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ListAgentsResponseSchema } from '../../../src/schemas/agent'; const modelProvider = { @@ -104,6 +105,7 @@ describe('agents router', () => { resolveMcpServerStore: () => new SqliteMcpServerStore(db), resolveSkillStore: () => new SqliteSkillStore(db), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), withTransaction: callback => db.transaction().execute(callback), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -114,6 +116,7 @@ describe('agents router', () => { resolveMcpServerStore: () => new SqliteMcpServerStore(db), resolveSkillStore: () => new SqliteSkillStore(db), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), withTransaction: callback => db.transaction().execute(callback), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: denyAllAuthorizer, diff --git a/packages/trueforge/tests/unit/apis/capabilities.test.ts b/packages/trueforge/tests/unit/apis/capabilities.test.ts index 720398a5a..763a97464 100644 --- a/packages/trueforge/tests/unit/apis/capabilities.test.ts +++ b/packages/trueforge/tests/unit/apis/capabilities.test.ts @@ -16,6 +16,7 @@ import type { OIDCConfig } from '../../../src/config'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; +import type { IWebSearchProviderStore } from '../../../src/db/webSearchProviderStore'; import { setCachedLocalSandboxSupport } from '../../../src/sandbox/localRuntime'; import { checkSnapshotStatus } from '../../../src/sandbox/providerUtils'; import type { SandboxBuildStatus, SandboxStatus } from '../../../src/schemas/sandboxProvider'; @@ -71,9 +72,17 @@ describe('capabilities routers', () => { function makeRouter(authenticator: Authenticator = new StandaloneAuthenticator()): OpenAPIHono { const db = createSqliteDb(':memory:'); + const emptyWebSearchStore: IWebSearchProviderStore = { + listProviders: () => Promise.resolve([]), + getProvider: () => Promise.resolve(undefined), + getProviderForUpdate: () => Promise.resolve(undefined), + createProvider: () => Promise.reject(new Error('not used')), + upsertProvider: () => Promise.reject(new Error('not used')), + }; return withAuth( createCapabilitiesRouter({ resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => emptyWebSearchStore, withTransaction: callback => db.transaction().execute(callback), logger: silentLogger, resolveRequestContext, @@ -247,9 +256,17 @@ describe('capabilities routers', () => { it('marks settings enabled for admin callers and disabled for non-admin callers', async () => { const db = createSqliteDb(':memory:'); await migrateSqliteToLatest(db); + const emptyWebSearchStore: IWebSearchProviderStore = { + listProviders: () => Promise.resolve([]), + getProvider: () => Promise.resolve(undefined), + getProviderForUpdate: () => Promise.resolve(undefined), + createProvider: () => Promise.reject(new Error('not used')), + upsertProvider: () => Promise.reject(new Error('not used')), + }; const router = withAuth( createCapabilitiesRouter({ resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => emptyWebSearchStore, withTransaction: callback => db.transaction().execute(callback), logger: silentLogger, resolveRequestContext, diff --git a/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts b/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts index 4c48f6f63..51b5c5ea6 100644 --- a/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts +++ b/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts @@ -17,6 +17,7 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; import { ListSessionsResponseSchema } from '../../../src/schemas/session'; @@ -38,6 +39,7 @@ describe('public CRUD after session deletion', () => { const skillStore = new SqliteSkillStore(db); const agentStore = new SqliteAgentStore(db); const sandboxProviderStore = new SqliteSandboxProviderStore(db); + const webSearchProviderStore = new SqliteWebSearchProviderStore(db); const app = new OpenAPIHono(); app.route( @@ -51,6 +53,7 @@ describe('public CRUD after session deletion', () => { resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, redis: createClient(), requestReplyRouter: new RequestReplyRouter(), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, @@ -70,6 +73,7 @@ describe('public CRUD after session deletion', () => { resolveAgentStore: () => agentStore, eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), diff --git a/packages/trueforge/tests/unit/apis/mcpServers.test.ts b/packages/trueforge/tests/unit/apis/mcpServers.test.ts index c950c9141..9c4f5dd33 100644 --- a/packages/trueforge/tests/unit/apis/mcpServers.test.ts +++ b/packages/trueforge/tests/unit/apis/mcpServers.test.ts @@ -7,6 +7,7 @@ import { McpCatalog } from '../../../src/catalog/McpCatalog'; import { ModelCatalog } from '../../../src/catalog/ModelCatalog'; import { SandboxCatalog } from '../../../src/catalog/SandboxCatalog'; import { SkillCatalog } from '../../../src/catalog/SkillCatalog'; +import { WebSearchCatalog } from '../../../src/catalog/WebSearchCatalog'; import configuration from '../../../src/config'; import { McpServerWithAuthStore } from '../../../src/db/McpServerWithAuthStore'; import type { IMcpServerWithAuthStore } from '../../../src/db/mcpServerStore'; @@ -143,6 +144,7 @@ describe('mcp-servers routers', () => { mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), }); mcpServersRouter = createMcpServersRouter({ resolveMcpServerStore: () => mcpServerStore, diff --git a/packages/trueforge/tests/unit/apis/modelProviders.test.ts b/packages/trueforge/tests/unit/apis/modelProviders.test.ts index ed725b0cd..23d5ec809 100644 --- a/packages/trueforge/tests/unit/apis/modelProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/modelProviders.test.ts @@ -8,6 +8,7 @@ import { McpCatalog } from '../../../src/catalog/McpCatalog'; import { ModelCatalog } from '../../../src/catalog/ModelCatalog'; import { SandboxCatalog } from '../../../src/catalog/SandboxCatalog'; import { SkillCatalog } from '../../../src/catalog/SkillCatalog'; +import { WebSearchCatalog } from '../../../src/catalog/WebSearchCatalog'; import configuration from '../../../src/config'; import { McpServerWithAuthStore } from '../../../src/db/McpServerWithAuthStore'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; @@ -18,6 +19,7 @@ import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider- import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { toRedactedSecretValue } from '../../../src/utils/secretRedaction'; const model = { @@ -130,6 +132,7 @@ async function createRouters(): Promise<{ tokenStore, resolveSkillStore: () => new SqliteSkillStore(db), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), withTransaction: callback => db.transaction().execute(callback), logger: winston.createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, @@ -139,6 +142,7 @@ async function createRouters(): Promise<{ mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), }), modelsRouter: createModelsRouter({ resolveModelProviderStore: () => modelProviderStore, diff --git a/packages/trueforge/tests/unit/apis/sandboxFileDownload.test.ts b/packages/trueforge/tests/unit/apis/sandboxFileDownload.test.ts index 3ad136a6c..87ba267ab 100644 --- a/packages/trueforge/tests/unit/apis/sandboxFileDownload.test.ts +++ b/packages/trueforge/tests/unit/apis/sandboxFileDownload.test.ts @@ -15,6 +15,7 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription'; @@ -51,6 +52,7 @@ async function buildApp() { resolveAgentStore: () => new SqliteAgentStore(db), eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), diff --git a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts index 56140983f..0be8b1c9a 100644 --- a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts @@ -17,6 +17,7 @@ import { McpCatalog } from '../../../src/catalog/McpCatalog'; import { ModelCatalog } from '../../../src/catalog/ModelCatalog'; import { SandboxCatalog } from '../../../src/catalog/SandboxCatalog'; import { SkillCatalog } from '../../../src/catalog/SkillCatalog'; +import { WebSearchCatalog } from '../../../src/catalog/WebSearchCatalog'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; import type { ISandboxProviderStore } from '../../../src/db/sandboxProviderStore'; import { createSqliteDb } from '../../../src/db/sqlite/client'; @@ -123,6 +124,7 @@ describe('sandboxProviders router', () => { mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), }); }); diff --git a/packages/trueforge/tests/unit/apis/schedules.test.ts b/packages/trueforge/tests/unit/apis/schedules.test.ts index af4b1bf1f..199272e38 100644 --- a/packages/trueforge/tests/unit/apis/schedules.test.ts +++ b/packages/trueforge/tests/unit/apis/schedules.test.ts @@ -72,6 +72,7 @@ function stubTurnExecutionDeps(agentStore: SqliteAgentStore, scheduleStore: Sqli resolveMcpServerStore: () => ({}) as never, turnSkillsResolverStore: { resolveTurnSkills: async () => [] }, resolveSandboxProviderStore: () => ({}) as never, + resolveWebSearchProviderStore: () => ({}) as never, }; } diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 43e94f0b4..5e64789f2 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -20,6 +20,7 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionMetricsStore } from '../../../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { ListSessionsResponseSchema } from '../../../src/schemas/session'; import { @@ -68,6 +69,7 @@ describe('sessions HTTP agent binding', () => { const mcpServerStore = new SqliteMcpServerStore(db); const skillStore = new SqliteSkillStore(db); const sandboxProviderStore = new SqliteSandboxProviderStore(db); + const webSearchProviderStore = new SqliteWebSearchProviderStore(db); agentStore = new SqliteAgentStore(db); await modelProviderStore.upsertProvider({ @@ -96,6 +98,7 @@ describe('sessions HTTP agent binding', () => { resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, redis: createClient(), requestReplyRouter: new RequestReplyRouter(), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, diff --git a/packages/trueforge/tests/unit/apis/skills.test.ts b/packages/trueforge/tests/unit/apis/skills.test.ts index c3d37cd95..8f637b472 100644 --- a/packages/trueforge/tests/unit/apis/skills.test.ts +++ b/packages/trueforge/tests/unit/apis/skills.test.ts @@ -5,6 +5,7 @@ import { McpCatalog } from '../../../src/catalog/McpCatalog'; import { ModelCatalog } from '../../../src/catalog/ModelCatalog'; import { SandboxCatalog } from '../../../src/catalog/SandboxCatalog'; import { SkillCatalog } from '../../../src/catalog/SkillCatalog'; +import { WebSearchCatalog } from '../../../src/catalog/WebSearchCatalog'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; @@ -62,6 +63,7 @@ describe('skills routers', () => { mcpCatalog: McpCatalog.load(), skillCatalog: SkillCatalog.load(), sandboxCatalog: SandboxCatalog.load(), + webSearchCatalog: WebSearchCatalog.load(), }); availableRouter = createAvailableSkillsRouter({ resolveSkillStore: () => skillStore, diff --git a/packages/trueforge/tests/unit/apis/turnHarnessErrorStatus.test.ts b/packages/trueforge/tests/unit/apis/turnHarnessErrorStatus.test.ts index eb270a7ca..a9e444870 100644 --- a/packages/trueforge/tests/unit/apis/turnHarnessErrorStatus.test.ts +++ b/packages/trueforge/tests/unit/apis/turnHarnessErrorStatus.test.ts @@ -15,6 +15,7 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; @@ -79,6 +80,7 @@ async function postTurnRejectingWith(error: AgentHarnessError): Promise new SqliteAgentStore(db), eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index 1d893d6e3..eb2e32da0 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -20,6 +20,7 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import type { Database } from '../../../src/db/sqlite/types'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; @@ -75,6 +76,7 @@ describe('turns', () => { resolveAgentStore: () => new SqliteAgentStore(db), eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -152,6 +154,7 @@ describe('turns', () => { resolveAgentStore: () => agentStore, eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: { @@ -233,6 +236,7 @@ describe('turns', () => { resolveAgentStore: () => new SqliteAgentStore(db), eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -339,6 +343,7 @@ describe('turns', () => { resolveSkillStore: () => new SqliteSkillStore(db), eventSubscriptions, resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger, resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -446,6 +451,7 @@ describe('turns', () => { resolveAgentStore: () => new SqliteAgentStore(db), eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger, resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -531,6 +537,7 @@ describe('turns', () => { resolveAgentStore: () => agentStore, eventSubscriptions: new EventSubscriptionRegistry(undefined), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveWebSearchProviderStore: () => new SqliteWebSearchProviderStore(db), logger: createLogger({ silent: true }), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer, diff --git a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts index c48863b1e..434fb641e 100644 --- a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts +++ b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts @@ -5,7 +5,6 @@ import { type SessionAgent, type SessionHandle, } from '@truefoundry/trueforge-core/agent-session'; -import { WebSearchProviders, type IWebSearchProvider } from '@truefoundry/trueforge-core/core'; import { HTTPException } from 'hono/http-exception'; import { validateGitAgentSkills } from '../../../src/db/gitSkillMounts'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; @@ -15,6 +14,7 @@ import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/Sq import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { buildGatewayMetadata, getModelDetails, @@ -28,18 +28,12 @@ import { } from '../../../src/runtime/sessionResources'; import { setCachedLocalSandboxSupport } from '../../../src/sandbox/localRuntime'; import type { ReasoningEffort } from '../../../src/schemas/modelProvider'; -import { resolveWebSearchProvider } from '../../../src/websearch/providers'; +import { hasConfiguredWebSearchProvider } from '../../../src/websearch/providers'; jest.mock('../../../src/websearch/providers', () => ({ - resolveWebSearchProvider: jest.fn(() => undefined), + hasConfiguredWebSearchProvider: jest.fn(() => Promise.resolve(false)), })); -const mockWebSearchProvider: IWebSearchProvider = { - id: WebSearchProviders.Parallel, - search: () => Promise.resolve({ hits: [] }), - fetch: () => Promise.resolve({ pages: [] }), -}; - async function createGatewayMetadataSession(input: { agent: SessionAgent }): Promise { const sessions = new Sessions({ sessionStore: new InMemorySessionStore() }); return sessions.create({ @@ -176,8 +170,8 @@ describe('localSandboxSessionSegment', () => { describe('validateAgentSpec', () => { afterEach(() => { setCachedLocalSandboxSupport(undefined); - jest.mocked(resolveWebSearchProvider).mockReset(); - jest.mocked(resolveWebSearchProvider).mockReturnValue(undefined); + jest.mocked(hasConfiguredWebSearchProvider).mockReset(); + jest.mocked(hasConfiguredWebSearchProvider).mockResolvedValue(false); }); async function setup(options?: { reasoningEfforts?: ReasoningEffort[] | undefined }) { @@ -211,6 +205,7 @@ describe('validateAgentSpec', () => { mcpServerStore: new SqliteMcpServerStore(db), skillStore: new SqliteSkillStore(db), sandboxProviderStore: new SqliteSandboxProviderStore(db), + webSearchProviderStore: new SqliteWebSearchProviderStore(db), }; } @@ -376,7 +371,7 @@ describe('validateAgentSpec', () => { it('admits web_search.enabled when a web-search provider resolves', async () => { const stores = await setup(); - jest.mocked(resolveWebSearchProvider).mockReturnValueOnce(mockWebSearchProvider); + jest.mocked(hasConfiguredWebSearchProvider).mockResolvedValueOnce(true); await expect( validateAgentSpec({ spec: AgentSpecSchema.parse({ From a9fce8eba6026c2718c70256777bc05b199be401 Mon Sep 17 00:00:00 2001 From: thesujai Date: Mon, 21 Sep 2026 21:40:23 +0530 Subject: [PATCH 02/10] feat: integrate ssrfFetch into ParallelWebSearchProvider for enhanced security --- .../src/core/web-search/ParallelWebSearchProvider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts index af846a385..5e1846ac9 100644 --- a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts +++ b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts @@ -1,4 +1,5 @@ import Parallel from 'parallel-web'; +import { ssrfFetch } from '../util/ssrfGuard'; import { WebSearchProviders, type IWebSearchProvider, @@ -28,7 +29,7 @@ export class ParallelWebSearchProvider implements IWebSearchProvider { readonly #mode: ParallelSearchMode; constructor(options: ParallelWebSearchProviderOptions) { - this.#client = new Parallel({ apiKey: options.apiKey }); + this.#client = new Parallel({ apiKey: options.apiKey, fetch: ssrfFetch }); this.#mode = options.mode; } From 5e465616d102f249a688c70701d15430eaac11ca Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Mon, 21 Sep 2026 16:16:04 +0000 Subject: [PATCH 03/10] Regenerate OpenAPI document and SDKs --- ...60921161604-regenerate-sdk-from-openapi.md | 5 + .github/fern/openapi/openapi.json | 351 +++++++++++++ docs/openapi.json | 351 +++++++++++++ packages/trueforge-sdk/package.json | 22 + packages/trueforge-sdk/reference.md | 254 ++++++++++ .../api/resources/catalogs/client/Client.ts | 6 + .../api/resources/catalogs/resources/index.ts | 1 + .../webSearchProviders/client/Client.ts | 109 ++++ .../webSearchProviders/client/index.ts | 1 + .../resources/webSearchProviders/exports.ts | 4 + .../resources/webSearchProviders/index.ts | 1 + .../api/resources/settings/client/Client.ts | 6 + .../api/resources/settings/resources/index.ts | 2 + .../webSearchProviders/client/Client.ts | 372 ++++++++++++++ .../webSearchProviders/client/index.ts | 1 + .../CreateWebSearchProviderRequest.ts | 19 + .../UpdateWebSearchProviderRequest.ts | 19 + .../client/requests/index.ts | 2 + .../resources/webSearchProviders/exports.ts | 4 + .../resources/webSearchProviders/index.ts | 1 + .../src/api/types/CatalogWebSearchProvider.ts | 9 + .../api/types/ConfiguredWebSearchProvider.ts | 8 + .../GetWebSearchProviderCatalogResponse.ts | 7 + .../api/types/GetWebSearchProviderResponse.ts | 7 + .../types/ListWebSearchProvidersResponse.ts | 7 + .../src/api/types/ParallelWebSearchMode.ts | 10 + .../types/ParallelWebSearchProviderAuth.ts | 9 + .../api/types/WebSearchProviderManifest.ts | 10 + packages/trueforge-sdk/src/api/types/index.ts | 8 + .../resources/settings/resources/index.ts | 2 + .../webSearchProviders/client/index.ts | 1 + .../CreateWebSearchProviderRequest.ts | 19 + .../UpdateWebSearchProviderRequest.ts | 19 + .../client/requests/index.ts | 2 + .../resources/webSearchProviders/index.ts | 1 + .../types/CatalogWebSearchProvider.ts | 21 + .../types/ConfiguredWebSearchProvider.ts | 22 + .../GetWebSearchProviderCatalogResponse.ts | 19 + .../types/GetWebSearchProviderResponse.ts | 19 + .../types/ListWebSearchProvidersResponse.ts | 19 + .../types/ParallelWebSearchMode.ts | 14 + .../types/ParallelWebSearchProviderAuth.ts | 18 + .../types/WebSearchProviderManifest.ts | 24 + .../src/serialization/types/index.ts | 8 + .../wire/catalogs/webSearchProviders.test.ts | 51 ++ .../wire/settings/webSearchProviders.test.ts | 298 +++++++++++ python/trueforge_sdk/reference.md | 282 +++++++++++ .../src/trueforge_sdk/__init__.py | 24 + .../src/trueforge_sdk/catalogs/__init__.py | 5 +- .../src/trueforge_sdk/catalogs/client.py | 19 + .../catalogs/web_search_providers/__init__.py | 4 + .../catalogs/web_search_providers/client.py | 104 ++++ .../web_search_providers/raw_client.py | 127 +++++ .../src/trueforge_sdk/settings/__init__.py | 5 +- .../src/trueforge_sdk/settings/client.py | 19 + .../settings/web_search_providers/__init__.py | 4 + .../settings/web_search_providers/client.py | 295 +++++++++++ .../web_search_providers/raw_client.py | 473 ++++++++++++++++++ .../src/trueforge_sdk/types/__init__.py | 24 + .../types/catalog_web_search_provider.py | 24 + .../types/configured_web_search_provider.py | 22 + ...et_web_search_provider_catalog_response.py | 20 + .../types/get_web_search_provider_response.py | 20 + .../list_web_search_providers_response.py | 20 + .../types/parallel_web_search_mode.py | 46 ++ .../parallel_web_search_provider_auth.py | 26 + .../types/web_search_provider_manifest.py | 26 + 67 files changed, 3748 insertions(+), 4 deletions(-) create mode 100644 .changeset/20260921161604-regenerate-sdk-from-openapi.md create mode 100644 packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts create mode 100644 packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/index.ts create mode 100644 packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/exports.ts create mode 100644 packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/index.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/index.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/exports.ts create mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/index.ts create mode 100644 packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts create mode 100644 packages/trueforge-sdk/src/api/types/ConfiguredWebSearchProvider.ts create mode 100644 packages/trueforge-sdk/src/api/types/GetWebSearchProviderCatalogResponse.ts create mode 100644 packages/trueforge-sdk/src/api/types/GetWebSearchProviderResponse.ts create mode 100644 packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts create mode 100644 packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts create mode 100644 packages/trueforge-sdk/src/api/types/ParallelWebSearchProviderAuth.ts create mode 100644 packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/index.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/index.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ConfiguredWebSearchProvider.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderCatalogResponse.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderResponse.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ParallelWebSearchProviderAuth.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts create mode 100644 packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts create mode 100644 packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts create mode 100644 python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/__init__.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/__init__.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/configured_web_search_provider.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_catalog_response.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_response.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_provider_auth.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py diff --git a/.changeset/20260921161604-regenerate-sdk-from-openapi.md b/.changeset/20260921161604-regenerate-sdk-from-openapi.md new file mode 100644 index 000000000..efd8ff00f --- /dev/null +++ b/.changeset/20260921161604-regenerate-sdk-from-openapi.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-sdk": patch +--- + +Regenerate SDK from updated OpenAPI spec. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index b9d8e2deb..be198e052 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -712,6 +712,26 @@ ], "type": "string" }, + "CatalogWebSearchProvider": { + "additionalProperties": false, + "properties": { + "mode": { + "$ref": "#/components/schemas/ParallelWebSearchMode" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type", + "mode" + ], + "type": "object" + }, "CatalogWellKnownModelProvider": { "additionalProperties": false, "properties": { @@ -982,6 +1002,22 @@ ], "type": "object" }, + "ConfiguredWebSearchProvider": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + } + }, + "required": [ + "name", + "manifest" + ], + "type": "object" + }, "ContextManagementConfig": { "default": { "compaction": { @@ -1151,6 +1187,18 @@ }, "type": "object" }, + "CreateWebSearchProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, "CreatedBySubject": { "additionalProperties": false, "description": "Who created this resource.", @@ -1638,6 +1686,31 @@ ], "type": "object" }, + "GetWebSearchProviderCatalogResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/CatalogWebSearchProvider" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "GetWebSearchProviderResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/ConfiguredWebSearchProvider" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "GitSkill": { "additionalProperties": false, "properties": { @@ -2470,6 +2543,20 @@ ], "type": "object" }, + "ListWebSearchProvidersResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ConfiguredWebSearchProvider" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "MCPAuthRequiredEvent": { "allOf": [ { @@ -3313,6 +3400,31 @@ ], "type": "object" }, + "ParallelWebSearchMode": { + "description": "Parallel Search mode preset.", + "enum": [ + "turbo", + "fast", + "basic", + "advanced" + ], + "type": "string" + }, + "ParallelWebSearchProviderAuth": { + "additionalProperties": false, + "description": "Parallel authentication credentials.", + "properties": { + "api_key": { + "description": "Parallel API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + }, "PermissionResourceType": { "description": "Resource type to evaluate permissions for.", "enum": [ @@ -5633,6 +5745,18 @@ ], "type": "object" }, + "UpdateWebSearchProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, "UserMessage": { "properties": { "content": { @@ -5769,6 +5893,30 @@ }, "type": "object" }, + "WebSearchProviderManifest": { + "additionalProperties": false, + "properties": { + "auth": { + "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" + }, + "mode": { + "$ref": "#/components/schemas/ParallelWebSearchMode" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type", + "auth", + "mode" + ], + "type": "object" + }, "ZaiModelProvider": { "additionalProperties": false, "properties": { @@ -7079,6 +7227,42 @@ "x-fern-sdk-method-name": "list" } }, + "/api/v1/catalogs/web-search-providers": { + "get": { + "description": "Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderCatalogResponse" + } + } + }, + "description": "Shipped web-search-provider presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the web-search provider catalog", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "catalogs", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "list" + } + }, "/api/v1/mcp-servers": { "get": { "description": "Configured MCP servers as a slim name/url list for the composer.", @@ -9966,6 +10150,170 @@ "x-fern-sdk-method-name": "create_or_update" } }, + "/api/v1/settings/web-search-providers": { + "get": { + "description": "All configured providers with nested manifests. `auth.api_key` is redacted.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListWebSearchProvidersResponse" + } + } + }, + "description": "All configured web-search providers" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "OIDC is configured and the request has no valid session cookie." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "OIDC is configured and the caller is authenticated but not an admin." + } + }, + "summary": "List configured web-search providers", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "list" + }, + "post": { + "description": "Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebSearchProviderRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderResponse" + } + } + }, + "description": "The created provider" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body, or redacted API key with no stored secret to keep." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "A web-search provider with this name already exists." + }, + "424": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unsupported operation because web-search providers are managed by an external system" + } + }, + "summary": "Create a web-search provider", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "create" + }, + "put": { + "description": "Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebSearchProviderRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderResponse" + } + } + }, + "description": "The saved provider" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body, or redacted API key with no stored secret to keep." + }, + "424": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unsupported operation because web-search providers are managed by an external system" + } + }, + "summary": "Create or replace a web-search provider", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "create_or_update" + } + }, "/api/v1/skills": { "get": { "description": "Configured skills as a slim name/description list for the composer.", @@ -10077,6 +10425,9 @@ { "name": "Sandboxes" }, + { + "name": "Web Search" + }, { "name": "Agents" }, diff --git a/docs/openapi.json b/docs/openapi.json index b9d8e2deb..be198e052 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -712,6 +712,26 @@ ], "type": "string" }, + "CatalogWebSearchProvider": { + "additionalProperties": false, + "properties": { + "mode": { + "$ref": "#/components/schemas/ParallelWebSearchMode" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type", + "mode" + ], + "type": "object" + }, "CatalogWellKnownModelProvider": { "additionalProperties": false, "properties": { @@ -982,6 +1002,22 @@ ], "type": "object" }, + "ConfiguredWebSearchProvider": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + } + }, + "required": [ + "name", + "manifest" + ], + "type": "object" + }, "ContextManagementConfig": { "default": { "compaction": { @@ -1151,6 +1187,18 @@ }, "type": "object" }, + "CreateWebSearchProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, "CreatedBySubject": { "additionalProperties": false, "description": "Who created this resource.", @@ -1638,6 +1686,31 @@ ], "type": "object" }, + "GetWebSearchProviderCatalogResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/CatalogWebSearchProvider" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "GetWebSearchProviderResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/ConfiguredWebSearchProvider" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "GitSkill": { "additionalProperties": false, "properties": { @@ -2470,6 +2543,20 @@ ], "type": "object" }, + "ListWebSearchProvidersResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ConfiguredWebSearchProvider" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "MCPAuthRequiredEvent": { "allOf": [ { @@ -3313,6 +3400,31 @@ ], "type": "object" }, + "ParallelWebSearchMode": { + "description": "Parallel Search mode preset.", + "enum": [ + "turbo", + "fast", + "basic", + "advanced" + ], + "type": "string" + }, + "ParallelWebSearchProviderAuth": { + "additionalProperties": false, + "description": "Parallel authentication credentials.", + "properties": { + "api_key": { + "description": "Parallel API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + }, "PermissionResourceType": { "description": "Resource type to evaluate permissions for.", "enum": [ @@ -5633,6 +5745,18 @@ ], "type": "object" }, + "UpdateWebSearchProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/WebSearchProviderManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, "UserMessage": { "properties": { "content": { @@ -5769,6 +5893,30 @@ }, "type": "object" }, + "WebSearchProviderManifest": { + "additionalProperties": false, + "properties": { + "auth": { + "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" + }, + "mode": { + "$ref": "#/components/schemas/ParallelWebSearchMode" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type", + "auth", + "mode" + ], + "type": "object" + }, "ZaiModelProvider": { "additionalProperties": false, "properties": { @@ -7079,6 +7227,42 @@ "x-fern-sdk-method-name": "list" } }, + "/api/v1/catalogs/web-search-providers": { + "get": { + "description": "Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderCatalogResponse" + } + } + }, + "description": "Shipped web-search-provider presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the web-search provider catalog", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "catalogs", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "list" + } + }, "/api/v1/mcp-servers": { "get": { "description": "Configured MCP servers as a slim name/url list for the composer.", @@ -9966,6 +10150,170 @@ "x-fern-sdk-method-name": "create_or_update" } }, + "/api/v1/settings/web-search-providers": { + "get": { + "description": "All configured providers with nested manifests. `auth.api_key` is redacted.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListWebSearchProvidersResponse" + } + } + }, + "description": "All configured web-search providers" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "OIDC is configured and the request has no valid session cookie." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "OIDC is configured and the caller is authenticated but not an admin." + } + }, + "summary": "List configured web-search providers", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "list" + }, + "post": { + "description": "Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebSearchProviderRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderResponse" + } + } + }, + "description": "The created provider" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body, or redacted API key with no stored secret to keep." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "A web-search provider with this name already exists." + }, + "424": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unsupported operation because web-search providers are managed by an external system" + } + }, + "summary": "Create a web-search provider", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "create" + }, + "put": { + "description": "Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebSearchProviderRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWebSearchProviderResponse" + } + } + }, + "description": "The saved provider" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body, or redacted API key with no stored secret to keep." + }, + "424": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unsupported operation because web-search providers are managed by an external system" + } + }, + "summary": "Create or replace a web-search provider", + "tags": [ + "Web Search" + ], + "x-fern-sdk-group-name": [ + "settings", + "webSearchProviders" + ], + "x-fern-sdk-method-name": "create_or_update" + } + }, "/api/v1/skills": { "get": { "description": "Configured skills as a slim name/description list for the composer.", @@ -10077,6 +10425,9 @@ { "name": "Sandboxes" }, + { + "name": "Web Search" + }, { "name": "Agents" }, diff --git a/packages/trueforge-sdk/package.json b/packages/trueforge-sdk/package.json index 13ccde5b7..6c6f4e463 100644 --- a/packages/trueforge-sdk/package.json +++ b/packages/trueforge-sdk/package.json @@ -187,6 +187,17 @@ }, "default": "./dist/cjs/api/resources/catalogs/resources/skills/exports.js" }, + "./catalogs/webSearchProviders": { + "import": { + "types": "./dist/esm/api/resources/catalogs/resources/webSearchProviders/exports.d.mts", + "default": "./dist/esm/api/resources/catalogs/resources/webSearchProviders/exports.mjs" + }, + "require": { + "types": "./dist/cjs/api/resources/catalogs/resources/webSearchProviders/exports.d.ts", + "default": "./dist/cjs/api/resources/catalogs/resources/webSearchProviders/exports.js" + }, + "default": "./dist/cjs/api/resources/catalogs/resources/webSearchProviders/exports.js" + }, "./internal/metrics": { "import": { "types": "./dist/esm/api/resources/internal/resources/metrics/exports.d.mts", @@ -286,6 +297,17 @@ }, "default": "./dist/cjs/api/resources/settings/resources/skills/exports.js" }, + "./settings/webSearchProviders": { + "import": { + "types": "./dist/esm/api/resources/settings/resources/webSearchProviders/exports.d.mts", + "default": "./dist/esm/api/resources/settings/resources/webSearchProviders/exports.mjs" + }, + "require": { + "types": "./dist/cjs/api/resources/settings/resources/webSearchProviders/exports.d.ts", + "default": "./dist/cjs/api/resources/settings/resources/webSearchProviders/exports.js" + }, + "default": "./dist/cjs/api/resources/settings/resources/webSearchProviders/exports.js" + }, "./package.json": "./package.json" }, "files": [ diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 0936210ae..7d209dfad 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -2793,6 +2793,62 @@ await client.catalogs.skills.list(); + + + + +## Catalogs WebSearchProviders +
client.catalogs.webSearchProviders.list() -> TrueForge.GetWebSearchProviderCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.catalogs.webSearchProviders.list(); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**requestOptions:** `WebSearchProvidersClient.RequestOptions` + +
+
+
+
+ +
@@ -3988,3 +4044,201 @@ await client.settings.skills.createOrUpdate({ +## Settings WebSearchProviders +
client.settings.webSearchProviders.list() -> TrueForge.ListWebSearchProvidersResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +All configured providers with nested manifests. `auth.api_key` is redacted. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.settings.webSearchProviders.list(); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**requestOptions:** `WebSearchProvidersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.settings.webSearchProviders.create({ ...params }) -> TrueForge.GetWebSearchProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.settings.webSearchProviders.create({ + manifest: { + auth: { + apiKey: "api_key" + }, + mode: "turbo", + type: "parallel" + } +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `TrueForge.settings.CreateWebSearchProviderRequest` + +
+
+ +
+
+ +**requestOptions:** `WebSearchProvidersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.settings.webSearchProviders.createOrUpdate({ ...params }) -> TrueForge.GetWebSearchProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.settings.webSearchProviders.createOrUpdate({ + manifest: { + auth: { + apiKey: "api_key" + }, + mode: "turbo", + type: "parallel" + } +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `TrueForge.settings.UpdateWebSearchProviderRequest` + +
+
+ +
+
+ +**requestOptions:** `WebSearchProvidersClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/client/Client.ts b/packages/trueforge-sdk/src/api/resources/catalogs/client/Client.ts index 79f2023b8..b3bef0e8e 100644 --- a/packages/trueforge-sdk/src/api/resources/catalogs/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/catalogs/client/Client.ts @@ -6,6 +6,7 @@ import { McpServersClient } from "../resources/mcpServers/client/Client.js"; import { ModelProvidersClient } from "../resources/modelProviders/client/Client.js"; import { SandboxProvidersClient } from "../resources/sandboxProviders/client/Client.js"; import { SkillsClient } from "../resources/skills/client/Client.js"; +import { WebSearchProvidersClient } from "../resources/webSearchProviders/client/Client.js"; export declare namespace CatalogsClient { export type Options = BaseClientOptions; @@ -17,6 +18,7 @@ export class CatalogsClient { protected _modelProviders: ModelProvidersClient | undefined; protected _sandboxProviders: SandboxProvidersClient | undefined; protected _skills: SkillsClient | undefined; + protected _webSearchProviders: WebSearchProvidersClient | undefined; constructor(options: CatalogsClient.Options) { this._options = normalizeClientOptionsWithAuth(options); @@ -37,4 +39,8 @@ export class CatalogsClient { public get skills(): SkillsClient { return (this._skills ??= new SkillsClient(this._options)); } + + public get webSearchProviders(): WebSearchProvidersClient { + return (this._webSearchProviders ??= new WebSearchProvidersClient(this._options)); + } } diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/index.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/index.ts index 160fb572f..e03815286 100644 --- a/packages/trueforge-sdk/src/api/resources/catalogs/resources/index.ts +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/index.ts @@ -2,3 +2,4 @@ export * as mcpServers from "./mcpServers/index.js"; export * as modelProviders from "./modelProviders/index.js"; export * as sandboxProviders from "./sandboxProviders/index.js"; export * as skills from "./skills/index.js"; +export * as webSearchProviders from "./webSearchProviders/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts new file mode 100644 index 000000000..3e5f20f7e --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts @@ -0,0 +1,109 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../../../core/headers.js"; +import * as core from "../../../../../../core/index.js"; +import { handleNonStatusCodeError } from "../../../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../../../errors/index.js"; +import * as serializers from "../../../../../../serialization/index.js"; +import * as TrueForge from "../../../../../index.js"; + +export declare namespace WebSearchProvidersClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class WebSearchProvidersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + + constructor(options: WebSearchProvidersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + * + * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.catalogs.webSearchProviders.list() + */ + public list( + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__list(requestOptions)); + } + + private async __list( + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/catalogs/web-search-providers", + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetWebSearchProviderCatalogResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/api/v1/catalogs/web-search-providers", + ); + } +} diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/index.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/exports.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/exports.ts new file mode 100644 index 000000000..8198c3c0d --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { WebSearchProvidersClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/index.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/settings/client/Client.ts b/packages/trueforge-sdk/src/api/resources/settings/client/Client.ts index f932bd78f..9b1809df1 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/client/Client.ts @@ -6,6 +6,7 @@ import { McpServersClient } from "../resources/mcpServers/client/Client.js"; import { ModelProvidersClient } from "../resources/modelProviders/client/Client.js"; import { SandboxProvidersClient } from "../resources/sandboxProviders/client/Client.js"; import { SkillsClient } from "../resources/skills/client/Client.js"; +import { WebSearchProvidersClient } from "../resources/webSearchProviders/client/Client.js"; export declare namespace SettingsClient { export type Options = BaseClientOptions; @@ -17,6 +18,7 @@ export class SettingsClient { protected _modelProviders: ModelProvidersClient | undefined; protected _sandboxProviders: SandboxProvidersClient | undefined; protected _skills: SkillsClient | undefined; + protected _webSearchProviders: WebSearchProvidersClient | undefined; constructor(options: SettingsClient.Options) { this._options = normalizeClientOptionsWithAuth(options); @@ -37,4 +39,8 @@ export class SettingsClient { public get skills(): SkillsClient { return (this._skills ??= new SkillsClient(this._options)); } + + public get webSearchProviders(): WebSearchProvidersClient { + return (this._webSearchProviders ??= new WebSearchProvidersClient(this._options)); + } } diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/index.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/index.ts index 61dea343e..bc60478e7 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/index.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/index.ts @@ -6,3 +6,5 @@ export * from "./sandboxProviders/client/requests/index.js"; export * as sandboxProviders from "./sandboxProviders/index.js"; export * from "./skills/client/requests/index.js"; export * as skills from "./skills/index.js"; +export * from "./webSearchProviders/client/requests/index.js"; +export * as webSearchProviders from "./webSearchProviders/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts new file mode 100644 index 000000000..59e23ab4e --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts @@ -0,0 +1,372 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../../../core/headers.js"; +import * as core from "../../../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../../../core/requestBody.js"; +import { handleNonStatusCodeError } from "../../../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../../../errors/index.js"; +import * as serializers from "../../../../../../serialization/index.js"; +import * as TrueForge from "../../../../../index.js"; + +export declare namespace WebSearchProvidersClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class WebSearchProvidersClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + + constructor(options: WebSearchProvidersClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * All configured providers with nested manifests. `auth.api_key` is redacted. + * + * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link TrueForge.ForbiddenError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.settings.webSearchProviders.list() + */ + public list( + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__list(requestOptions)); + } + + private async __list( + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/settings/web-search-providers", + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.ListWebSearchProvidersResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 403: + throw new TrueForge.ForbiddenError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/api/v1/settings/web-search-providers", + ); + } + + /** + * Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + * + * @param {TrueForge.settings.CreateWebSearchProviderRequest} request + * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.ConflictError} + * @throws {@link TrueForge.FailedDependencyError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.settings.webSearchProviders.create({ + * manifest: { + * auth: { + * apiKey: "api_key" + * }, + * mode: "turbo", + * type: "parallel" + * } + * }) + */ + public create( + request: TrueForge.settings.CreateWebSearchProviderRequest, + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); + } + + private async __create( + request: TrueForge.settings.CreateWebSearchProviderRequest, + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/settings/web-search-providers", + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.settings.CreateWebSearchProviderRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetWebSearchProviderResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 409: + throw new TrueForge.ConflictError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 424: + throw new TrueForge.FailedDependencyError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/api/v1/settings/web-search-providers", + ); + } + + /** + * Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + * + * @param {TrueForge.settings.UpdateWebSearchProviderRequest} request + * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.FailedDependencyError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.settings.webSearchProviders.createOrUpdate({ + * manifest: { + * auth: { + * apiKey: "api_key" + * }, + * mode: "turbo", + * type: "parallel" + * } + * }) + */ + public createOrUpdate( + request: TrueForge.settings.UpdateWebSearchProviderRequest, + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__createOrUpdate(request, requestOptions)); + } + + private async __createOrUpdate( + request: TrueForge.settings.UpdateWebSearchProviderRequest, + requestOptions?: WebSearchProvidersClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/settings/web-search-providers", + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.settings.UpdateWebSearchProviderRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetWebSearchProviderResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 424: + throw new TrueForge.FailedDependencyError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PUT", + "/api/v1/settings/web-search-providers", + ); + } +} diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/index.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts new file mode 100644 index 000000000..c8f06a67f --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../../index.js"; + +/** + * @example + * { + * manifest: { + * auth: { + * apiKey: "api_key" + * }, + * mode: "turbo", + * type: "parallel" + * } + * } + */ +export interface CreateWebSearchProviderRequest { + manifest: TrueForge.WebSearchProviderManifest; +} diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts new file mode 100644 index 000000000..6da2c3a81 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../../index.js"; + +/** + * @example + * { + * manifest: { + * auth: { + * apiKey: "api_key" + * }, + * mode: "turbo", + * type: "parallel" + * } + * } + */ +export interface UpdateWebSearchProviderRequest { + manifest: TrueForge.WebSearchProviderManifest; +} diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts new file mode 100644 index 000000000..d9efecc50 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { CreateWebSearchProviderRequest } from "./CreateWebSearchProviderRequest.js"; +export type { UpdateWebSearchProviderRequest } from "./UpdateWebSearchProviderRequest.js"; diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/exports.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/exports.ts new file mode 100644 index 000000000..8198c3c0d --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { WebSearchProvidersClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/index.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts b/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts new file mode 100644 index 000000000..54351e696 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface CatalogWebSearchProvider { + mode: TrueForge.ParallelWebSearchMode; + /** Parallel web-search provider. */ + type: "parallel"; +} diff --git a/packages/trueforge-sdk/src/api/types/ConfiguredWebSearchProvider.ts b/packages/trueforge-sdk/src/api/types/ConfiguredWebSearchProvider.ts new file mode 100644 index 000000000..1d7c4c214 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ConfiguredWebSearchProvider.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface ConfiguredWebSearchProvider { + manifest: TrueForge.WebSearchProviderManifest; + name: TrueForge.ResourceName; +} diff --git a/packages/trueforge-sdk/src/api/types/GetWebSearchProviderCatalogResponse.ts b/packages/trueforge-sdk/src/api/types/GetWebSearchProviderCatalogResponse.ts new file mode 100644 index 000000000..4234a59c8 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/GetWebSearchProviderCatalogResponse.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface GetWebSearchProviderCatalogResponse { + data: TrueForge.CatalogWebSearchProvider[]; +} diff --git a/packages/trueforge-sdk/src/api/types/GetWebSearchProviderResponse.ts b/packages/trueforge-sdk/src/api/types/GetWebSearchProviderResponse.ts new file mode 100644 index 000000000..8e60d753e --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/GetWebSearchProviderResponse.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface GetWebSearchProviderResponse { + data: TrueForge.ConfiguredWebSearchProvider; +} diff --git a/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts b/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts new file mode 100644 index 000000000..f2389877c --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface ListWebSearchProvidersResponse { + data: TrueForge.ConfiguredWebSearchProvider[]; +} diff --git a/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts b/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts new file mode 100644 index 000000000..b078158bd --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +/** Parallel Search mode preset. */ +export const ParallelWebSearchMode = { + Turbo: "turbo", + Fast: "fast", + Basic: "basic", + Advanced: "advanced", +} as const; +export type ParallelWebSearchMode = (typeof ParallelWebSearchMode)[keyof typeof ParallelWebSearchMode]; diff --git a/packages/trueforge-sdk/src/api/types/ParallelWebSearchProviderAuth.ts b/packages/trueforge-sdk/src/api/types/ParallelWebSearchProviderAuth.ts new file mode 100644 index 000000000..b22edb01e --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ParallelWebSearchProviderAuth.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * Parallel authentication credentials. + */ +export interface ParallelWebSearchProviderAuth { + /** Parallel API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key. */ + apiKey: string; +} diff --git a/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts new file mode 100644 index 000000000..c64bd7060 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface WebSearchProviderManifest { + auth: TrueForge.ParallelWebSearchProviderAuth; + mode: TrueForge.ParallelWebSearchMode; + /** Parallel web-search provider. */ + type: "parallel"; +} diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index 9b29138b5..e87768f70 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -29,6 +29,7 @@ export * from "./CatalogModelProvider.js"; export * from "./CatalogSandboxProvider.js"; export * from "./CatalogSkill.js"; export * from "./CatalogSkillType.js"; +export * from "./CatalogWebSearchProvider.js"; export * from "./CatalogWellKnownModelProvider.js"; export * from "./CatalogWellKnownModelProviderType.js"; export * from "./ChatCompletionChunkDeltaToolCall.js"; @@ -43,6 +44,7 @@ export * from "./ConfiguredModel.js"; export * from "./ConfiguredModelProvider.js"; export * from "./ConfiguredSandboxProvider.js"; export * from "./ConfiguredSkill.js"; +export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; export * from "./CreateScheduleRunResponse.js"; @@ -78,6 +80,8 @@ export * from "./GetSessionResponse.js"; export * from "./GetSkillCatalogResponse.js"; export * from "./GetSkillResponse.js"; export * from "./GetTurnResponse.js"; +export * from "./GetWebSearchProviderCatalogResponse.js"; +export * from "./GetWebSearchProviderResponse.js"; export * from "./GitSkill.js"; export * from "./GoogleGeminiModelProvider.js"; export * from "./InitialUserMessage.js"; @@ -102,6 +106,7 @@ export * from "./ListSkillVersionsResponse.js"; export * from "./ListTurnEventsOrder.js"; export * from "./ListTurnEventsResponse.js"; export * from "./ListTurnsResponse.js"; +export * from "./ListWebSearchProvidersResponse.js"; export * from "./McpAuthRequiredEvent.js"; export * from "./McpAuthStatus.js"; export * from "./McpAuthStatusStatus.js"; @@ -136,6 +141,8 @@ export * from "./ModelProviderAuth.js"; export * from "./ModelProviderManifest.js"; export * from "./MoonshotModelProvider.js"; export * from "./OpenAiModelProvider.js"; +export * from "./ParallelWebSearchMode.js"; +export * from "./ParallelWebSearchProviderAuth.js"; export * from "./PermissionResourceType.js"; export * from "./PreviousTurnIdInput.js"; export * from "./RawToolCall.js"; @@ -234,4 +241,5 @@ export * from "./UserToolApprovalEvent.js"; export * from "./UserToolResponseEvent.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; +export * from "./WebSearchProviderManifest.js"; export * from "./ZaiModelProvider.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/index.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/index.ts index 61dea343e..bc60478e7 100644 --- a/packages/trueforge-sdk/src/serialization/resources/settings/resources/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/index.ts @@ -6,3 +6,5 @@ export * from "./sandboxProviders/client/requests/index.js"; export * as sandboxProviders from "./sandboxProviders/index.js"; export * from "./skills/client/requests/index.js"; export * as skills from "./skills/index.js"; +export * from "./webSearchProviders/client/requests/index.js"; +export * as webSearchProviders from "./webSearchProviders/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/index.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts new file mode 100644 index 000000000..b074760a3 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../../../api/index.js"; +import * as core from "../../../../../../../core/index.js"; +import type * as serializers from "../../../../../../index.js"; +import { WebSearchProviderManifest } from "../../../../../../types/WebSearchProviderManifest.js"; + +export const CreateWebSearchProviderRequest: core.serialization.Schema< + serializers.settings.CreateWebSearchProviderRequest.Raw, + TrueForge.settings.CreateWebSearchProviderRequest +> = core.serialization.object({ + manifest: WebSearchProviderManifest, +}); + +export declare namespace CreateWebSearchProviderRequest { + export interface Raw { + manifest: WebSearchProviderManifest.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts new file mode 100644 index 000000000..758108d85 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../../../api/index.js"; +import * as core from "../../../../../../../core/index.js"; +import type * as serializers from "../../../../../../index.js"; +import { WebSearchProviderManifest } from "../../../../../../types/WebSearchProviderManifest.js"; + +export const UpdateWebSearchProviderRequest: core.serialization.Schema< + serializers.settings.UpdateWebSearchProviderRequest.Raw, + TrueForge.settings.UpdateWebSearchProviderRequest +> = core.serialization.object({ + manifest: WebSearchProviderManifest, +}); + +export declare namespace UpdateWebSearchProviderRequest { + export interface Raw { + manifest: WebSearchProviderManifest.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts new file mode 100644 index 000000000..5c31ff327 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts @@ -0,0 +1,2 @@ +export { CreateWebSearchProviderRequest } from "./CreateWebSearchProviderRequest.js"; +export { UpdateWebSearchProviderRequest } from "./UpdateWebSearchProviderRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/index.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts b/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts new file mode 100644 index 000000000..6b3dc8ef4 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ParallelWebSearchMode } from "./ParallelWebSearchMode.js"; + +export const CatalogWebSearchProvider: core.serialization.ObjectSchema< + serializers.CatalogWebSearchProvider.Raw, + TrueForge.CatalogWebSearchProvider +> = core.serialization.object({ + mode: ParallelWebSearchMode, + type: core.serialization.stringLiteral("parallel"), +}); + +export declare namespace CatalogWebSearchProvider { + export interface Raw { + mode: ParallelWebSearchMode.Raw; + type: "parallel"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ConfiguredWebSearchProvider.ts b/packages/trueforge-sdk/src/serialization/types/ConfiguredWebSearchProvider.ts new file mode 100644 index 000000000..698c294d0 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ConfiguredWebSearchProvider.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ResourceName } from "./ResourceName.js"; +import { WebSearchProviderManifest } from "./WebSearchProviderManifest.js"; + +export const ConfiguredWebSearchProvider: core.serialization.ObjectSchema< + serializers.ConfiguredWebSearchProvider.Raw, + TrueForge.ConfiguredWebSearchProvider +> = core.serialization.object({ + manifest: WebSearchProviderManifest, + name: ResourceName, +}); + +export declare namespace ConfiguredWebSearchProvider { + export interface Raw { + manifest: WebSearchProviderManifest.Raw; + name: ResourceName.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderCatalogResponse.ts b/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderCatalogResponse.ts new file mode 100644 index 000000000..436c52914 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderCatalogResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { CatalogWebSearchProvider } from "./CatalogWebSearchProvider.js"; + +export const GetWebSearchProviderCatalogResponse: core.serialization.ObjectSchema< + serializers.GetWebSearchProviderCatalogResponse.Raw, + TrueForge.GetWebSearchProviderCatalogResponse +> = core.serialization.object({ + data: core.serialization.list(CatalogWebSearchProvider), +}); + +export declare namespace GetWebSearchProviderCatalogResponse { + export interface Raw { + data: CatalogWebSearchProvider.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderResponse.ts b/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderResponse.ts new file mode 100644 index 000000000..006ca039a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/GetWebSearchProviderResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ConfiguredWebSearchProvider } from "./ConfiguredWebSearchProvider.js"; + +export const GetWebSearchProviderResponse: core.serialization.ObjectSchema< + serializers.GetWebSearchProviderResponse.Raw, + TrueForge.GetWebSearchProviderResponse +> = core.serialization.object({ + data: ConfiguredWebSearchProvider, +}); + +export declare namespace GetWebSearchProviderResponse { + export interface Raw { + data: ConfiguredWebSearchProvider.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts b/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts new file mode 100644 index 000000000..3e2142424 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ConfiguredWebSearchProvider } from "./ConfiguredWebSearchProvider.js"; + +export const ListWebSearchProvidersResponse: core.serialization.ObjectSchema< + serializers.ListWebSearchProvidersResponse.Raw, + TrueForge.ListWebSearchProvidersResponse +> = core.serialization.object({ + data: core.serialization.list(ConfiguredWebSearchProvider), +}); + +export declare namespace ListWebSearchProvidersResponse { + export interface Raw { + data: ConfiguredWebSearchProvider.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts b/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts new file mode 100644 index 000000000..f9fd6a773 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const ParallelWebSearchMode: core.serialization.Schema< + serializers.ParallelWebSearchMode.Raw, + TrueForge.ParallelWebSearchMode +> = core.serialization.enum_(["turbo", "fast", "basic", "advanced"]); + +export declare namespace ParallelWebSearchMode { + export type Raw = "turbo" | "fast" | "basic" | "advanced"; +} diff --git a/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchProviderAuth.ts b/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchProviderAuth.ts new file mode 100644 index 000000000..98604a17f --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchProviderAuth.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const ParallelWebSearchProviderAuth: core.serialization.ObjectSchema< + serializers.ParallelWebSearchProviderAuth.Raw, + TrueForge.ParallelWebSearchProviderAuth +> = core.serialization.object({ + apiKey: core.serialization.property("api_key", core.serialization.string()), +}); + +export declare namespace ParallelWebSearchProviderAuth { + export interface Raw { + api_key: string; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts new file mode 100644 index 000000000..d8bc89420 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ParallelWebSearchMode } from "./ParallelWebSearchMode.js"; +import { ParallelWebSearchProviderAuth } from "./ParallelWebSearchProviderAuth.js"; + +export const WebSearchProviderManifest: core.serialization.ObjectSchema< + serializers.WebSearchProviderManifest.Raw, + TrueForge.WebSearchProviderManifest +> = core.serialization.object({ + auth: ParallelWebSearchProviderAuth, + mode: ParallelWebSearchMode, + type: core.serialization.stringLiteral("parallel"), +}); + +export declare namespace WebSearchProviderManifest { + export interface Raw { + auth: ParallelWebSearchProviderAuth.Raw; + mode: ParallelWebSearchMode.Raw; + type: "parallel"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index 9b29138b5..e87768f70 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -29,6 +29,7 @@ export * from "./CatalogModelProvider.js"; export * from "./CatalogSandboxProvider.js"; export * from "./CatalogSkill.js"; export * from "./CatalogSkillType.js"; +export * from "./CatalogWebSearchProvider.js"; export * from "./CatalogWellKnownModelProvider.js"; export * from "./CatalogWellKnownModelProviderType.js"; export * from "./ChatCompletionChunkDeltaToolCall.js"; @@ -43,6 +44,7 @@ export * from "./ConfiguredModel.js"; export * from "./ConfiguredModelProvider.js"; export * from "./ConfiguredSandboxProvider.js"; export * from "./ConfiguredSkill.js"; +export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; export * from "./CreateScheduleRunResponse.js"; @@ -78,6 +80,8 @@ export * from "./GetSessionResponse.js"; export * from "./GetSkillCatalogResponse.js"; export * from "./GetSkillResponse.js"; export * from "./GetTurnResponse.js"; +export * from "./GetWebSearchProviderCatalogResponse.js"; +export * from "./GetWebSearchProviderResponse.js"; export * from "./GitSkill.js"; export * from "./GoogleGeminiModelProvider.js"; export * from "./InitialUserMessage.js"; @@ -102,6 +106,7 @@ export * from "./ListSkillVersionsResponse.js"; export * from "./ListTurnEventsOrder.js"; export * from "./ListTurnEventsResponse.js"; export * from "./ListTurnsResponse.js"; +export * from "./ListWebSearchProvidersResponse.js"; export * from "./McpAuthRequiredEvent.js"; export * from "./McpAuthStatus.js"; export * from "./McpAuthStatusStatus.js"; @@ -136,6 +141,8 @@ export * from "./ModelProviderAuth.js"; export * from "./ModelProviderManifest.js"; export * from "./MoonshotModelProvider.js"; export * from "./OpenAiModelProvider.js"; +export * from "./ParallelWebSearchMode.js"; +export * from "./ParallelWebSearchProviderAuth.js"; export * from "./PermissionResourceType.js"; export * from "./PreviousTurnIdInput.js"; export * from "./RawToolCall.js"; @@ -234,4 +241,5 @@ export * from "./UserToolApprovalEvent.js"; export * from "./UserToolResponseEvent.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; +export * from "./WebSearchProviderManifest.js"; export * from "./ZaiModelProvider.js"; diff --git a/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts b/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts new file mode 100644 index 000000000..0db47cfe9 --- /dev/null +++ b/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts @@ -0,0 +1,51 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as TrueForgeTypes from "../../../src/api/index"; +import { TrueForge } from "../../../src/Client"; +import { mockServerPool } from "../../mock-server/MockServerPool"; + +describe("WebSearchProvidersClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { data: [{ mode: "turbo", type: "parallel" }] }; + + server + .mockEndpoint() + .get("/api/v1/catalogs/web-search-providers") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.catalogs.webSearchProviders.list(); + expect(response).toEqual({ + data: [ + { + mode: "turbo", + type: "parallel", + }, + ], + }); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/catalogs/web-search-providers") + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.catalogs.webSearchProviders.list(); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); +}); diff --git a/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts new file mode 100644 index 000000000..5b25cb3a1 --- /dev/null +++ b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts @@ -0,0 +1,298 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as TrueForgeTypes from "../../../src/api/index"; +import { TrueForge } from "../../../src/Client"; +import { mockServerPool } from "../../mock-server/MockServerPool"; + +describe("WebSearchProvidersClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { + data: [{ manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }], + }; + + server + .mockEndpoint() + .get("/api/v1/settings/web-search-providers") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.settings.webSearchProviders.list(); + expect(response).toEqual({ + data: [ + { + manifest: { + auth: { + apiKey: "api_key", + }, + mode: "turbo", + type: "parallel", + }, + name: "name", + }, + ], + }); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/settings/web-search-providers") + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.list(); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/settings/web-search-providers") + .respondWith() + .statusCode(403) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.list(); + }).rejects.toThrow(TrueForgeTypes.ForbiddenError); + }); + + test("create (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { + data: { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }, + }; + + server + .mockEndpoint() + .post("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.settings.webSearchProviders.create({ + manifest: { + auth: { + apiKey: "api_key", + }, + mode: "turbo", + type: "parallel", + }, + }); + expect(response).toEqual({ + data: { + manifest: { + auth: { + apiKey: "api_key", + }, + mode: "turbo", + type: "parallel", + }, + name: "name", + }, + }); + }); + + test("create (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.create({ + manifest: { + auth: { + apiKey: "x", + }, + mode: "turbo", + type: "parallel", + }, + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("create (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(409) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.create({ + manifest: { + auth: { + apiKey: "x", + }, + mode: "turbo", + type: "parallel", + }, + }); + }).rejects.toThrow(TrueForgeTypes.ConflictError); + }); + + test("create (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(424) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.create({ + manifest: { + auth: { + apiKey: "x", + }, + mode: "turbo", + type: "parallel", + }, + }); + }).rejects.toThrow(TrueForgeTypes.FailedDependencyError); + }); + + test("create_or_update (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { + data: { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }, + }; + + server + .mockEndpoint() + .put("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.settings.webSearchProviders.createOrUpdate({ + manifest: { + auth: { + apiKey: "api_key", + }, + mode: "turbo", + type: "parallel", + }, + }); + expect(response).toEqual({ + data: { + manifest: { + auth: { + apiKey: "api_key", + }, + mode: "turbo", + type: "parallel", + }, + name: "name", + }, + }); + }); + + test("create_or_update (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.createOrUpdate({ + manifest: { + auth: { + apiKey: "x", + }, + mode: "turbo", + type: "parallel", + }, + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("create_or_update (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/settings/web-search-providers") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(424) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.settings.webSearchProviders.createOrUpdate({ + manifest: { + auth: { + apiKey: "x", + }, + mode: "turbo", + type: "parallel", + }, + }); + }).rejects.toThrow(TrueForgeTypes.FailedDependencyError); + }); +}); diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index aa8c83132..61643cd7e 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -3303,6 +3303,69 @@ client.catalogs.skills.list() + + + + +## Catalogs WebSearchProviders +
client.catalogs.web_search_providers.list() -> GetWebSearchProviderCatalogResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.catalogs.web_search_providers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
@@ -4690,3 +4753,222 @@ client.settings.skills.create_or_update( +## Settings WebSearchProviders +
client.settings.web_search_providers.list() -> ListWebSearchProvidersResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +All configured providers with nested manifests. `auth.api_key` is redacted. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.web_search_providers.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.web_search_providers.create(...) -> GetWebSearchProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, WebSearchProviderManifest, ParallelWebSearchProviderAuth, ParallelWebSearchMode + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.web_search_providers.create( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + type="parallel", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `WebSearchProviderManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.settings.web_search_providers.create_or_update(...) -> GetWebSearchProviderResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, WebSearchProviderManifest, ParallelWebSearchProviderAuth, ParallelWebSearchMode + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.settings.web_search_providers.create_or_update( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + type="parallel", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `WebSearchProviderManifest` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index 7242c3c63..ce622a1eb 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -38,6 +38,7 @@ CatalogSandboxProvider, CatalogSkill, CatalogSkillType, + CatalogWebSearchProvider, CatalogWellKnownModelProvider, CatalogWellKnownModelProviderType, ChatCompletionChunkDeltaToolCall, @@ -52,6 +53,7 @@ ConfiguredModelProvider, ConfiguredSandboxProvider, ConfiguredSkill, + ConfiguredWebSearchProvider, ContextManagementConfig, CreateScheduleRunResponse, CreateSessionAgent, @@ -87,6 +89,8 @@ GetSkillCatalogResponse, GetSkillResponse, GetTurnResponse, + GetWebSearchProviderCatalogResponse, + GetWebSearchProviderResponse, GitSkill, GoogleGeminiModelProvider, InitialUserMessage, @@ -111,6 +115,7 @@ ListTurnEventsOrder, ListTurnEventsResponse, ListTurnsResponse, + ListWebSearchProvidersResponse, McpAuthRequiredEvent, McpAuthStatus, McpAuthStatusStatus, @@ -145,6 +150,8 @@ ModelProviderManifest, MoonshotModelProvider, OpenAiModelProvider, + ParallelWebSearchMode, + ParallelWebSearchProviderAuth, PermissionResourceType, PreviousTurnIdInput, RawToolCall, @@ -243,6 +250,7 @@ UserToolResponseEvent, WebSearchCapability, WebSearchConfig, + WebSearchProviderManifest, ZaiModelProvider, ) from .errors import ( @@ -298,6 +306,7 @@ "CatalogSandboxProvider": ".types", "CatalogSkill": ".types", "CatalogSkillType": ".types", + "CatalogWebSearchProvider": ".types", "CatalogWellKnownModelProvider": ".types", "CatalogWellKnownModelProviderType": ".types", "ChatCompletionChunkDeltaToolCall": ".types", @@ -312,6 +321,7 @@ "ConfiguredModelProvider": ".types", "ConfiguredSandboxProvider": ".types", "ConfiguredSkill": ".types", + "ConfiguredWebSearchProvider": ".types", "ConflictError": ".errors", "ContentTooLargeError": ".errors", "ContextManagementConfig": ".types", @@ -353,6 +363,8 @@ "GetSkillCatalogResponse": ".types", "GetSkillResponse": ".types", "GetTurnResponse": ".types", + "GetWebSearchProviderCatalogResponse": ".types", + "GetWebSearchProviderResponse": ".types", "GitSkill": ".types", "GoneError": ".errors", "GoogleGeminiModelProvider": ".types", @@ -379,6 +391,7 @@ "ListTurnEventsOrder": ".types", "ListTurnEventsResponse": ".types", "ListTurnsResponse": ".types", + "ListWebSearchProvidersResponse": ".types", "McpAuthRequiredEvent": ".types", "McpAuthStatus": ".types", "McpAuthStatusStatus": ".types", @@ -414,6 +427,8 @@ "MoonshotModelProvider": ".types", "NotFoundError": ".errors", "OpenAiModelProvider": ".types", + "ParallelWebSearchMode": ".types", + "ParallelWebSearchProviderAuth": ".types", "PermissionResourceType": ".types", "PreconditionFailedError": ".errors", "PreviousTurnIdInput": ".types", @@ -516,6 +531,7 @@ "UserToolResponseEvent": ".types", "WebSearchCapability": ".types", "WebSearchConfig": ".types", + "WebSearchProviderManifest": ".types", "ZaiModelProvider": ".types", "__version__": ".version", "agents": ".agents", @@ -588,6 +604,7 @@ def __dir__(): "CatalogSandboxProvider", "CatalogSkill", "CatalogSkillType", + "CatalogWebSearchProvider", "CatalogWellKnownModelProvider", "CatalogWellKnownModelProviderType", "ChatCompletionChunkDeltaToolCall", @@ -602,6 +619,7 @@ def __dir__(): "ConfiguredModelProvider", "ConfiguredSandboxProvider", "ConfiguredSkill", + "ConfiguredWebSearchProvider", "ConflictError", "ContentTooLargeError", "ContextManagementConfig", @@ -643,6 +661,8 @@ def __dir__(): "GetSkillCatalogResponse", "GetSkillResponse", "GetTurnResponse", + "GetWebSearchProviderCatalogResponse", + "GetWebSearchProviderResponse", "GitSkill", "GoneError", "GoogleGeminiModelProvider", @@ -669,6 +689,7 @@ def __dir__(): "ListTurnEventsOrder", "ListTurnEventsResponse", "ListTurnsResponse", + "ListWebSearchProvidersResponse", "McpAuthRequiredEvent", "McpAuthStatus", "McpAuthStatusStatus", @@ -704,6 +725,8 @@ def __dir__(): "MoonshotModelProvider", "NotFoundError", "OpenAiModelProvider", + "ParallelWebSearchMode", + "ParallelWebSearchProviderAuth", "PermissionResourceType", "PreconditionFailedError", "PreviousTurnIdInput", @@ -806,6 +829,7 @@ def __dir__(): "UserToolResponseEvent", "WebSearchCapability", "WebSearchConfig", + "WebSearchProviderManifest", "ZaiModelProvider", "__version__", "agents", diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py index c8555edfe..0755f2665 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/__init__.py @@ -6,12 +6,13 @@ from importlib import import_module if typing.TYPE_CHECKING: - from . import mcp_servers, model_providers, sandbox_providers, skills + from . import mcp_servers, model_providers, sandbox_providers, skills, web_search_providers _dynamic_imports: typing.Dict[str, str] = { "mcp_servers": ".mcp_servers", "model_providers": ".model_providers", "sandbox_providers": ".sandbox_providers", "skills": ".skills", + "web_search_providers": ".web_search_providers", } @@ -36,4 +37,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills"] +__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills", "web_search_providers"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py index d74c24af1..9d97a177c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/client.py @@ -12,6 +12,7 @@ from .model_providers.client import AsyncModelProvidersClient, ModelProvidersClient from .sandbox_providers.client import AsyncSandboxProvidersClient, SandboxProvidersClient from .skills.client import AsyncSkillsClient, SkillsClient + from .web_search_providers.client import AsyncWebSearchProvidersClient, WebSearchProvidersClient class CatalogsClient: @@ -22,6 +23,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._model_providers: typing.Optional[ModelProvidersClient] = None self._sandbox_providers: typing.Optional[SandboxProvidersClient] = None self._skills: typing.Optional[SkillsClient] = None + self._web_search_providers: typing.Optional[WebSearchProvidersClient] = None @property def with_raw_response(self) -> RawCatalogsClient: @@ -66,6 +68,14 @@ def skills(self): self._skills = SkillsClient(client_wrapper=self._client_wrapper) return self._skills + @property + def web_search_providers(self): + if self._web_search_providers is None: + from .web_search_providers.client import WebSearchProvidersClient # noqa: E402 + + self._web_search_providers = WebSearchProvidersClient(client_wrapper=self._client_wrapper) + return self._web_search_providers + class AsyncCatalogsClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -75,6 +85,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._model_providers: typing.Optional[AsyncModelProvidersClient] = None self._sandbox_providers: typing.Optional[AsyncSandboxProvidersClient] = None self._skills: typing.Optional[AsyncSkillsClient] = None + self._web_search_providers: typing.Optional[AsyncWebSearchProvidersClient] = None @property def with_raw_response(self) -> AsyncRawCatalogsClient: @@ -118,3 +129,11 @@ def skills(self): self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) return self._skills + + @property + def web_search_providers(self): + if self._web_search_providers is None: + from .web_search_providers.client import AsyncWebSearchProvidersClient # noqa: E402 + + self._web_search_providers = AsyncWebSearchProvidersClient(client_wrapper=self._client_wrapper) + return self._web_search_providers diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py new file mode 100644 index 000000000..2ee84b4b7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py @@ -0,0 +1,104 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_web_search_provider_catalog_response import GetWebSearchProviderCatalogResponse +from .raw_client import AsyncRawWebSearchProvidersClient, RawWebSearchProvidersClient + + +class WebSearchProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawWebSearchProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawWebSearchProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawWebSearchProvidersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetWebSearchProviderCatalogResponse: + """ + Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderCatalogResponse + Shipped web-search-provider presets. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.catalogs.web_search_providers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncWebSearchProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawWebSearchProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawWebSearchProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawWebSearchProvidersClient + """ + return self._raw_client + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetWebSearchProviderCatalogResponse: + """ + Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderCatalogResponse + Shipped web-search-provider presets. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.catalogs.web_search_providers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py new file mode 100644 index 000000000..3f1c19981 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_web_search_provider_catalog_response import GetWebSearchProviderCatalogResponse +from ...types.request_error_response import RequestErrorResponse +from pydantic import ValidationError + + +class RawWebSearchProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetWebSearchProviderCatalogResponse]: + """ + Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetWebSearchProviderCatalogResponse] + Shipped web-search-provider presets. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/catalogs/web-search-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderCatalogResponse, + construct_type( + type_=GetWebSearchProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawWebSearchProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetWebSearchProviderCatalogResponse]: + """ + Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetWebSearchProviderCatalogResponse] + Shipped web-search-provider presets. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/catalogs/web-search-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderCatalogResponse, + construct_type( + type_=GetWebSearchProviderCatalogResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py index c8555edfe..0755f2665 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/__init__.py @@ -6,12 +6,13 @@ from importlib import import_module if typing.TYPE_CHECKING: - from . import mcp_servers, model_providers, sandbox_providers, skills + from . import mcp_servers, model_providers, sandbox_providers, skills, web_search_providers _dynamic_imports: typing.Dict[str, str] = { "mcp_servers": ".mcp_servers", "model_providers": ".model_providers", "sandbox_providers": ".sandbox_providers", "skills": ".skills", + "web_search_providers": ".web_search_providers", } @@ -36,4 +37,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills"] +__all__ = ["mcp_servers", "model_providers", "sandbox_providers", "skills", "web_search_providers"] diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/client.py index 26014cb68..d37bd7ade 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/settings/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/client.py @@ -12,6 +12,7 @@ from .model_providers.client import AsyncModelProvidersClient, ModelProvidersClient from .sandbox_providers.client import AsyncSandboxProvidersClient, SandboxProvidersClient from .skills.client import AsyncSkillsClient, SkillsClient + from .web_search_providers.client import AsyncWebSearchProvidersClient, WebSearchProvidersClient class SettingsClient: @@ -22,6 +23,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._model_providers: typing.Optional[ModelProvidersClient] = None self._sandbox_providers: typing.Optional[SandboxProvidersClient] = None self._skills: typing.Optional[SkillsClient] = None + self._web_search_providers: typing.Optional[WebSearchProvidersClient] = None @property def with_raw_response(self) -> RawSettingsClient: @@ -66,6 +68,14 @@ def skills(self): self._skills = SkillsClient(client_wrapper=self._client_wrapper) return self._skills + @property + def web_search_providers(self): + if self._web_search_providers is None: + from .web_search_providers.client import WebSearchProvidersClient # noqa: E402 + + self._web_search_providers = WebSearchProvidersClient(client_wrapper=self._client_wrapper) + return self._web_search_providers + class AsyncSettingsClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -75,6 +85,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._model_providers: typing.Optional[AsyncModelProvidersClient] = None self._sandbox_providers: typing.Optional[AsyncSandboxProvidersClient] = None self._skills: typing.Optional[AsyncSkillsClient] = None + self._web_search_providers: typing.Optional[AsyncWebSearchProvidersClient] = None @property def with_raw_response(self) -> AsyncRawSettingsClient: @@ -118,3 +129,11 @@ def skills(self): self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) return self._skills + + @property + def web_search_providers(self): + if self._web_search_providers is None: + from .web_search_providers.client import AsyncWebSearchProvidersClient # noqa: E402 + + self._web_search_providers = AsyncWebSearchProvidersClient(client_wrapper=self._client_wrapper) + return self._web_search_providers diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py new file mode 100644 index 000000000..1ae9bcba8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py @@ -0,0 +1,295 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ...types.get_web_search_provider_response import GetWebSearchProviderResponse +from ...types.list_web_search_providers_response import ListWebSearchProvidersResponse +from ...types.web_search_provider_manifest import WebSearchProviderManifest +from .raw_client import AsyncRawWebSearchProvidersClient, RawWebSearchProvidersClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class WebSearchProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawWebSearchProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawWebSearchProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawWebSearchProvidersClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListWebSearchProvidersResponse: + """ + All configured providers with nested manifests. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListWebSearchProvidersResponse + All configured web-search providers + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.web_search_providers.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def create( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetWebSearchProviderResponse: + """ + Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderResponse + The created provider + + Examples + -------- + from trueforge_sdk import ( + ParallelWebSearchMode, + ParallelWebSearchProviderAuth, + TrueForge, + WebSearchProviderManifest, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.web_search_providers.create( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + ), + ) + """ + _response = self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + def create_or_update( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetWebSearchProviderResponse: + """ + Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderResponse + The saved provider + + Examples + -------- + from trueforge_sdk import ( + ParallelWebSearchMode, + ParallelWebSearchProviderAuth, + TrueForge, + WebSearchProviderManifest, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.settings.web_search_providers.create_or_update( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + ), + ) + """ + _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data + + +class AsyncWebSearchProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawWebSearchProvidersClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawWebSearchProvidersClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawWebSearchProvidersClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListWebSearchProvidersResponse: + """ + All configured providers with nested manifests. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListWebSearchProvidersResponse + All configured web-search providers + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.web_search_providers.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def create( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetWebSearchProviderResponse: + """ + Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderResponse + The created provider + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AsyncTrueForge, + ParallelWebSearchMode, + ParallelWebSearchProviderAuth, + WebSearchProviderManifest, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.web_search_providers.create( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(manifest=manifest, request_options=request_options) + return _response.data + + async def create_or_update( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> GetWebSearchProviderResponse: + """ + Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetWebSearchProviderResponse + The saved provider + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AsyncTrueForge, + ParallelWebSearchMode, + ParallelWebSearchProviderAuth, + WebSearchProviderManifest, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.settings.web_search_providers.create_or_update( + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + mode=ParallelWebSearchMode.TURBO, + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_or_update(manifest=manifest, request_options=request_options) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py new file mode 100644 index 000000000..c43c0f18a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py @@ -0,0 +1,473 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ...core.unchecked_base_model import construct_type +from ...errors.bad_request_error import BadRequestError +from ...errors.conflict_error import ConflictError +from ...errors.failed_dependency_error import FailedDependencyError +from ...errors.forbidden_error import ForbiddenError +from ...errors.unauthorized_error import UnauthorizedError +from ...types.get_web_search_provider_response import GetWebSearchProviderResponse +from ...types.list_web_search_providers_response import ListWebSearchProvidersResponse +from ...types.request_error_response import RequestErrorResponse +from ...types.web_search_provider_manifest import WebSearchProviderManifest +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawWebSearchProvidersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ListWebSearchProvidersResponse]: + """ + All configured providers with nested manifests. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListWebSearchProvidersResponse] + All configured web-search providers + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListWebSearchProvidersResponse, + construct_type( + type_=ListWebSearchProvidersResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetWebSearchProviderResponse]: + """ + Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetWebSearchProviderResponse] + The created provider + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=WebSearchProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderResponse, + construct_type( + type_=GetWebSearchProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_or_update( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetWebSearchProviderResponse]: + """ + Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetWebSearchProviderResponse] + The saved provider + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=WebSearchProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderResponse, + construct_type( + type_=GetWebSearchProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawWebSearchProvidersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ListWebSearchProvidersResponse]: + """ + All configured providers with nested manifests. `auth.api_key` is redacted. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListWebSearchProvidersResponse] + All configured web-search providers + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListWebSearchProvidersResponse, + construct_type( + type_=ListWebSearchProvidersResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetWebSearchProviderResponse]: + """ + Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetWebSearchProviderResponse] + The created provider + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="POST", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=WebSearchProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderResponse, + construct_type( + type_=GetWebSearchProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_or_update( + self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetWebSearchProviderResponse]: + """ + Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + + Parameters + ---------- + manifest : WebSearchProviderManifest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetWebSearchProviderResponse] + The saved provider + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/settings/web-search-providers", + method="PUT", + json={ + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=WebSearchProviderManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetWebSearchProviderResponse, + construct_type( + type_=GetWebSearchProviderResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 424: + raise FailedDependencyError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index 2e83715b6..04094eaf4 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -37,6 +37,7 @@ from .catalog_sandbox_provider import CatalogSandboxProvider from .catalog_skill import CatalogSkill from .catalog_skill_type import CatalogSkillType + from .catalog_web_search_provider import CatalogWebSearchProvider from .catalog_well_known_model_provider import CatalogWellKnownModelProvider from .catalog_well_known_model_provider_type import CatalogWellKnownModelProviderType from .chat_completion_chunk_delta_tool_call import ChatCompletionChunkDeltaToolCall @@ -51,6 +52,7 @@ from .configured_model_provider import ConfiguredModelProvider from .configured_sandbox_provider import ConfiguredSandboxProvider from .configured_skill import ConfiguredSkill + from .configured_web_search_provider import ConfiguredWebSearchProvider from .context_management_config import ContextManagementConfig from .create_schedule_run_response import CreateScheduleRunResponse from .create_session_agent import CreateSessionAgent @@ -86,6 +88,8 @@ from .get_skill_catalog_response import GetSkillCatalogResponse from .get_skill_response import GetSkillResponse from .get_turn_response import GetTurnResponse + from .get_web_search_provider_catalog_response import GetWebSearchProviderCatalogResponse + from .get_web_search_provider_response import GetWebSearchProviderResponse from .git_skill import GitSkill from .google_gemini_model_provider import GoogleGeminiModelProvider from .initial_user_message import InitialUserMessage @@ -110,6 +114,7 @@ from .list_turn_events_order import ListTurnEventsOrder from .list_turn_events_response import ListTurnEventsResponse from .list_turns_response import ListTurnsResponse + from .list_web_search_providers_response import ListWebSearchProvidersResponse from .mcp_auth_required_event import McpAuthRequiredEvent from .mcp_auth_status import McpAuthStatus from .mcp_auth_status_status import McpAuthStatusStatus @@ -144,6 +149,8 @@ from .model_provider_manifest import ModelProviderManifest from .moonshot_model_provider import MoonshotModelProvider from .open_ai_model_provider import OpenAiModelProvider + from .parallel_web_search_mode import ParallelWebSearchMode + from .parallel_web_search_provider_auth import ParallelWebSearchProviderAuth from .permission_resource_type import PermissionResourceType from .previous_turn_id_input import PreviousTurnIdInput from .raw_tool_call import RawToolCall @@ -242,6 +249,7 @@ from .user_tool_response_event import UserToolResponseEvent from .web_search_capability import WebSearchCapability from .web_search_config import WebSearchConfig + from .web_search_provider_manifest import WebSearchProviderManifest from .zai_model_provider import ZaiModelProvider _dynamic_imports: typing.Dict[str, str] = { "ActionRequired": ".action_required", @@ -275,6 +283,7 @@ "CatalogSandboxProvider": ".catalog_sandbox_provider", "CatalogSkill": ".catalog_skill", "CatalogSkillType": ".catalog_skill_type", + "CatalogWebSearchProvider": ".catalog_web_search_provider", "CatalogWellKnownModelProvider": ".catalog_well_known_model_provider", "CatalogWellKnownModelProviderType": ".catalog_well_known_model_provider_type", "ChatCompletionChunkDeltaToolCall": ".chat_completion_chunk_delta_tool_call", @@ -289,6 +298,7 @@ "ConfiguredModelProvider": ".configured_model_provider", "ConfiguredSandboxProvider": ".configured_sandbox_provider", "ConfiguredSkill": ".configured_skill", + "ConfiguredWebSearchProvider": ".configured_web_search_provider", "ContextManagementConfig": ".context_management_config", "CreateScheduleRunResponse": ".create_schedule_run_response", "CreateSessionAgent": ".create_session_agent", @@ -324,6 +334,8 @@ "GetSkillCatalogResponse": ".get_skill_catalog_response", "GetSkillResponse": ".get_skill_response", "GetTurnResponse": ".get_turn_response", + "GetWebSearchProviderCatalogResponse": ".get_web_search_provider_catalog_response", + "GetWebSearchProviderResponse": ".get_web_search_provider_response", "GitSkill": ".git_skill", "GoogleGeminiModelProvider": ".google_gemini_model_provider", "InitialUserMessage": ".initial_user_message", @@ -348,6 +360,7 @@ "ListTurnEventsOrder": ".list_turn_events_order", "ListTurnEventsResponse": ".list_turn_events_response", "ListTurnsResponse": ".list_turns_response", + "ListWebSearchProvidersResponse": ".list_web_search_providers_response", "McpAuthRequiredEvent": ".mcp_auth_required_event", "McpAuthStatus": ".mcp_auth_status", "McpAuthStatusStatus": ".mcp_auth_status_status", @@ -382,6 +395,8 @@ "ModelProviderManifest": ".model_provider_manifest", "MoonshotModelProvider": ".moonshot_model_provider", "OpenAiModelProvider": ".open_ai_model_provider", + "ParallelWebSearchMode": ".parallel_web_search_mode", + "ParallelWebSearchProviderAuth": ".parallel_web_search_provider_auth", "PermissionResourceType": ".permission_resource_type", "PreviousTurnIdInput": ".previous_turn_id_input", "RawToolCall": ".raw_tool_call", @@ -480,6 +495,7 @@ "UserToolResponseEvent": ".user_tool_response_event", "WebSearchCapability": ".web_search_capability", "WebSearchConfig": ".web_search_config", + "WebSearchProviderManifest": ".web_search_provider_manifest", "ZaiModelProvider": ".zai_model_provider", } @@ -537,6 +553,7 @@ def __dir__(): "CatalogSandboxProvider", "CatalogSkill", "CatalogSkillType", + "CatalogWebSearchProvider", "CatalogWellKnownModelProvider", "CatalogWellKnownModelProviderType", "ChatCompletionChunkDeltaToolCall", @@ -551,6 +568,7 @@ def __dir__(): "ConfiguredModelProvider", "ConfiguredSandboxProvider", "ConfiguredSkill", + "ConfiguredWebSearchProvider", "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", @@ -586,6 +604,8 @@ def __dir__(): "GetSkillCatalogResponse", "GetSkillResponse", "GetTurnResponse", + "GetWebSearchProviderCatalogResponse", + "GetWebSearchProviderResponse", "GitSkill", "GoogleGeminiModelProvider", "InitialUserMessage", @@ -610,6 +630,7 @@ def __dir__(): "ListTurnEventsOrder", "ListTurnEventsResponse", "ListTurnsResponse", + "ListWebSearchProvidersResponse", "McpAuthRequiredEvent", "McpAuthStatus", "McpAuthStatusStatus", @@ -644,6 +665,8 @@ def __dir__(): "ModelProviderManifest", "MoonshotModelProvider", "OpenAiModelProvider", + "ParallelWebSearchMode", + "ParallelWebSearchProviderAuth", "PermissionResourceType", "PreviousTurnIdInput", "RawToolCall", @@ -742,5 +765,6 @@ def __dir__(): "UserToolResponseEvent", "WebSearchCapability", "WebSearchConfig", + "WebSearchProviderManifest", "ZaiModelProvider", ] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py new file mode 100644 index 000000000..6e315caae --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .parallel_web_search_mode import ParallelWebSearchMode + + +class CatalogWebSearchProvider(UncheckedBaseModel): + mode: ParallelWebSearchMode + type: typing.Literal["parallel"] = pydantic.Field(default="parallel") + """ + Parallel web-search provider. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_web_search_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_web_search_provider.py new file mode 100644 index 000000000..025d45e50 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_web_search_provider.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .resource_name import ResourceName +from .web_search_provider_manifest import WebSearchProviderManifest + + +class ConfiguredWebSearchProvider(UncheckedBaseModel): + manifest: WebSearchProviderManifest + name: ResourceName + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_catalog_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_catalog_response.py new file mode 100644 index 000000000..4115cef9b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_catalog_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .catalog_web_search_provider import CatalogWebSearchProvider + + +class GetWebSearchProviderCatalogResponse(UncheckedBaseModel): + data: typing.List[CatalogWebSearchProvider] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_response.py new file mode 100644 index 000000000..3e77b1ea0 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_web_search_provider_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_web_search_provider import ConfiguredWebSearchProvider + + +class GetWebSearchProviderResponse(UncheckedBaseModel): + data: ConfiguredWebSearchProvider + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py new file mode 100644 index 000000000..abd50453c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .configured_web_search_provider import ConfiguredWebSearchProvider + + +class ListWebSearchProvidersResponse(UncheckedBaseModel): + data: typing.List[ConfiguredWebSearchProvider] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py b/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py new file mode 100644 index 000000000..632c3d7bf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ParallelWebSearchMode(enum.StrEnum): + """ + Parallel Search mode preset. + """ + + TURBO = "turbo" + FAST = "fast" + BASIC = "basic" + ADVANCED = "advanced" + _UNKNOWN = "__PARALLELWEBSEARCHMODE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "ParallelWebSearchMode": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + turbo: typing.Callable[[], T_Result], + fast: typing.Callable[[], T_Result], + basic: typing.Callable[[], T_Result], + advanced: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is ParallelWebSearchMode.TURBO: + return turbo() + if self is ParallelWebSearchMode.FAST: + return fast() + if self is ParallelWebSearchMode.BASIC: + return basic() + if self is ParallelWebSearchMode.ADVANCED: + return advanced() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_provider_auth.py b/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_provider_auth.py new file mode 100644 index 000000000..9e89e933a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_provider_auth.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ParallelWebSearchProviderAuth(UncheckedBaseModel): + """ + Parallel authentication credentials. + """ + + api_key: str = pydantic.Field() + """ + Parallel API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py new file mode 100644 index 000000000..84d169259 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .parallel_web_search_mode import ParallelWebSearchMode +from .parallel_web_search_provider_auth import ParallelWebSearchProviderAuth + + +class WebSearchProviderManifest(UncheckedBaseModel): + auth: ParallelWebSearchProviderAuth + mode: ParallelWebSearchMode + type: typing.Literal["parallel"] = pydantic.Field(default="parallel") + """ + Parallel web-search provider. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow From 3ee2edaae3338f2edf114c8e5036b2b40be543df Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Tue, 22 Sep 2026 07:37:24 +0000 Subject: [PATCH 04/10] Regenerate OpenAPI document and SDKs --- .github/fern/openapi/openapi.json | 2 +- docs/openapi.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index be198e052..50bf3dc56 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -5964,7 +5964,7 @@ "info": { "description": "HTTP API for the TrueForge agent server (`/api/v1`). Interactive docs are served at `/api/v1/docs` (OpenAPI JSON at `/api/v1/openapi.json`).\n\n**Authentication:** Standalone auth accepts requests without credentials — middleware stamps a local default user. When OIDC or TrueFoundry auth is configured, protected routes require a valid cookie or `Authorization: Bearer` token. There is no built-in API-key scheme; pass custom headers only if your reverse proxy or IdP layer requires them.\n\nCovers DB-backed sessions, the agent registry, settings catalogs, and model/MCP/skill/sandbox providers.", "title": "TrueForge API", - "version": "0.2.0" + "version": "0.2.1" }, "openapi": "3.1.0", "paths": { diff --git a/docs/openapi.json b/docs/openapi.json index be198e052..50bf3dc56 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -5964,7 +5964,7 @@ "info": { "description": "HTTP API for the TrueForge agent server (`/api/v1`). Interactive docs are served at `/api/v1/docs` (OpenAPI JSON at `/api/v1/openapi.json`).\n\n**Authentication:** Standalone auth accepts requests without credentials — middleware stamps a local default user. When OIDC or TrueFoundry auth is configured, protected routes require a valid cookie or `Authorization: Bearer` token. There is no built-in API-key scheme; pass custom headers only if your reverse proxy or IdP layer requires them.\n\nCovers DB-backed sessions, the agent registry, settings catalogs, and model/MCP/skill/sandbox providers.", "title": "TrueForge API", - "version": "0.2.0" + "version": "0.2.1" }, "openapi": "3.1.0", "paths": { From 7191077d3e1cc4535573af175a549a3117e21c0b Mon Sep 17 00:00:00 2001 From: Govinda Vashishtha <57435703+govindavashishtha@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:44:19 +0530 Subject: [PATCH 05/10] Implement web search provider settings UI and API integration - Introduced a new settings UI for configuring web search providers, allowing admins to set API keys and modes for Parallel web search in standalone/OIDC deployments. - Added necessary types and interfaces for web search provider management in the assistant-ui-runtime and trueforge-ui packages. - Created backend catalog functions to handle web search provider operations, including creation and updates. - Updated the settings builder to include web search as a configurable section, enhancing the overall settings experience. This update enhances the admin capabilities for managing web search integrations within the application. --- .changeset/web-search-settings-ui.md | 6 + packages/assistant-ui-runtime/src/index.ts | 12 + .../assistant-ui-runtime/src/server/index.ts | 12 + .../assistant-ui-runtime/src/server/types.ts | 70 ++++- .../AgentSessionMetricsStrip.tsx | 2 +- .../ConfigureWebSearchForm.tsx | 169 ++++++++++ .../SettingsBuilder/WebSearchSettings.tsx | 289 ++++++++++++++++++ .../src/containers/SettingsBuilder/index.tsx | 17 +- packages/trueforge-ui/src/index.ts | 7 + .../catalogs/webSearchProviderCatalog.ts | 145 +++++++++ .../trueforge-agent-server-adapter/index.ts | 10 + .../src/server/ShellModeContext.tsx | 2 +- packages/trueforge-ui/src/server/types.ts | 10 + .../WebSearchSettings.test.tsx | 187 ++++++++++++ .../containers/SettingsBuilder/index.test.tsx | 31 +- .../catalogs/webSearchProviderCatalog.test.ts | 73 +++++ 16 files changed, 1032 insertions(+), 10 deletions(-) create mode 100644 .changeset/web-search-settings-ui.md create mode 100644 packages/trueforge-ui/src/containers/SettingsBuilder/ConfigureWebSearchForm.tsx create mode 100644 packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx create mode 100644 packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts create mode 100644 packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx create mode 100644 packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts diff --git a/.changeset/web-search-settings-ui.md b/.changeset/web-search-settings-ui.md new file mode 100644 index 000000000..aac45172a --- /dev/null +++ b/.changeset/web-search-settings-ui.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge-assistant-ui-runtime": minor +"@truefoundry/trueforge-ui": minor +--- + +Add web-search provider settings catalog port and Settings UI so admins can configure Parallel web search (API key + mode) in standalone/OIDC deployments. diff --git a/packages/assistant-ui-runtime/src/index.ts b/packages/assistant-ui-runtime/src/index.ts index 383f49f38..d56cfdf8f 100644 --- a/packages/assistant-ui-runtime/src/index.ts +++ b/packages/assistant-ui-runtime/src/index.ts @@ -130,6 +130,8 @@ export type { CreateSessionRequest, CreateSkillRequest, CreateSkillRequestBase, + CreateWebSearchProviderRequest, + CreateWebSearchRequest, CreatedBySubject, DefinedSkill, GithubSkill, @@ -156,6 +158,7 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, + ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -212,7 +215,16 @@ export type { UpdateSandboxRequest, UpdateScheduleRequest, UpdateSessionRequest, + UpdateWebSearchProviderRequest, + UpdateWebSearchRequest, UserMessage, UserToolApprovalEvent, UserToolResponseEvent, + WebSearchBase, + WebSearchCatalogEntry, + WebSearchCatalogServer, + WebSearchConfig, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, } from './server/index.js'; diff --git a/packages/assistant-ui-runtime/src/server/index.ts b/packages/assistant-ui-runtime/src/server/index.ts index 0364daeb5..c8742e0ce 100644 --- a/packages/assistant-ui-runtime/src/server/index.ts +++ b/packages/assistant-ui-runtime/src/server/index.ts @@ -56,6 +56,8 @@ export type { CreateSessionRequest, CreateSkillRequest, CreateSkillRequestBase, + CreateWebSearchProviderRequest, + CreateWebSearchRequest, CreatedBySubject, DefinedSkill, GithubSkill, @@ -80,6 +82,7 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, + ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -130,10 +133,19 @@ export type { UpdateSandboxRequest, UpdateScheduleRequest, UpdateSessionRequest, + UpdateWebSearchProviderRequest, + UpdateWebSearchRequest, UserMessage, UserMessageContent, UserToolApprovalEvent, UserToolResponseEvent, + WebSearchBase, + WebSearchCatalogEntry, + WebSearchCatalogServer, + WebSearchConfig, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, } from './types.js'; export type { diff --git a/packages/assistant-ui-runtime/src/server/types.ts b/packages/assistant-ui-runtime/src/server/types.ts index 7ad5a2b94..8e6cbac63 100644 --- a/packages/assistant-ui-runtime/src/server/types.ts +++ b/packages/assistant-ui-runtime/src/server/types.ts @@ -794,6 +794,71 @@ export interface SandboxCatalogServer< deleteSandboxProvider?(req: { id: string }): Promise; } +// --------------------------------------------------------------------------- +// Web-search providers catalog — public rows omit credentials; writes accept them +// --------------------------------------------------------------------------- + +/** + * Parallel Search mode preset (Parallel-specific, not a global web-search mode). + * A future provider would carry its own config fields instead of sharing this enum. + */ +export type ParallelWebSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced'; + +/** Mutable web-search settings shared by catalog rows, create, and update. */ +export interface WebSearchConfig { + mode: ParallelWebSearchMode; +} + +export interface WebSearchCatalogEntry extends WebSearchConfig { + id: string; + name: string; + type: string; +} + +/** + * Connected web-search provider row (settings/web-search). No raw `apiKey`. + * Includes last-saved config so update forms can show previous values. + */ +export interface WebSearchBase extends WebSearchConfig { + id: string; + name: string; + catalogId: string; + isConnected: boolean; +} + +export interface CreateWebSearchRequest extends WebSearchConfig { + /** `WebSearchCatalogEntry.id` used to create this web-search provider. */ + catalogId: string; + name: string; + type: string; + apiKey: string; +} + +export interface UpdateWebSearchRequest extends WebSearchConfig { + id: string; + /** Omit to keep the existing key; send a value to rotate. */ + apiKey?: string; +} + +/** Host-facing aliases (trueforge-ui public names). */ +export type WebSearchProviderConfig = WebSearchConfig; +export type WebSearchProviderCatalogEntry = WebSearchCatalogEntry; +export type WebSearchProviderBase = WebSearchBase; +export type CreateWebSearchProviderRequest = CreateWebSearchRequest; +export type UpdateWebSearchProviderRequest = UpdateWebSearchRequest; + +export interface WebSearchCatalogServer< + TProvider extends WebSearchBase = WebSearchBase, + TCatalogEntry extends WebSearchCatalogEntry = WebSearchCatalogEntry, + TCreate extends CreateWebSearchRequest = CreateWebSearchRequest, + TUpdate extends UpdateWebSearchRequest = UpdateWebSearchRequest, +> { + getWebSearchProviderCatalog(): Promise; + listWebSearchProviders(req?: { query?: string }): Promise; + createWebSearchProvider(req: TCreate): Promise; + updateWebSearchProvider(req: TUpdate): Promise; +} + /** Host-facing selector / compose aliases (trueforge-ui public names). */ export type ModelSelection = ModelSelectorEntry; export type AgentSkill = SkillSelectorEntry; @@ -803,7 +868,7 @@ export type SearchAgentsParams = SearchAgentSelectorParams; /** * Settings management aggregate — modelCatalog + connectorCatalog + optional - * skill and sandbox catalogs. + * skill, sandbox, and web-search catalogs. * Hosts may pass the whole object to an app shell, or a focused sub-port to a page. */ export interface CatalogServer< @@ -811,6 +876,7 @@ export interface CatalogServer< TConnectorCatalog extends ConnectorCatalogServer = ConnectorCatalogServer, TSkillCatalog extends SkillCatalogServer = SkillCatalogServer, TSandboxCatalog extends SandboxCatalogServer = SandboxCatalogServer, + TWebSearchCatalog extends WebSearchCatalogServer = WebSearchCatalogServer, > { modelCatalog: TModelCatalog; connectorCatalog: TConnectorCatalog; @@ -818,6 +884,8 @@ export interface CatalogServer< skillCatalog?: TSkillCatalog; /** Optional — omit when the host has no sandboxes settings surface. */ sandboxCatalog?: TSandboxCatalog; + /** Optional — omit when the host has no web-search settings surface. */ + webSearchCatalog?: TWebSearchCatalog; } // --------------------------------------------------------------------------- diff --git a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionMetricsStrip.tsx b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionMetricsStrip.tsx index e23d74237..f2fbf268f 100644 --- a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionMetricsStrip.tsx +++ b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionMetricsStrip.tsx @@ -55,7 +55,7 @@ export function AgentSessionMetricsStrip({ metrics }: AgentSessionMetricsStripPr metrics.totalCostUsd == null ? '@min-[48rem]:grid-cols-7' : '@min-[48rem]:grid-cols-8', )} > - + 1 ? 'Turns' : 'Turn'} value={metrics.totalTurns} /> void; + onSave: (draft: WebSearchConfigDraft) => void | Promise; + title: string; + description?: string; + /** Prefills config fields; apiKey is never autofilled. */ + initialConfig?: WebSearchProviderConfig | null; + /** When false (updates), empty apiKey means keep the existing key. */ + requireApiKey?: boolean; + busy?: boolean; + error?: string | null; +}; + +const MODE_OPTIONS: Array<{ value: ParallelWebSearchMode; label: string }> = [ + { value: 'turbo', label: 'Turbo' }, + { value: 'fast', label: 'Fast' }, + { value: 'basic', label: 'Basic' }, + { value: 'advanced', label: 'Advanced' }, +]; + +const EMPTY_CONFIG: WebSearchProviderConfig = { + mode: 'turbo', +}; + +const inputClassName = auiInputClass('h-11 shadow-sm'); + +function isParallelWebSearchMode(value: string): value is ParallelWebSearchMode { + return MODE_OPTIONS.some(option => option.value === value); +} + +const ConfigureWebSearchForm = ({ + open, + onOpenChange, + onSave, + title, + description, + initialConfig = null, + requireApiKey = true, + busy = false, + error, +}: ConfigureWebSearchFormProps) => { + const [mode, setMode] = useState(EMPTY_CONFIG.mode); + const [apiKey, setApiKey] = useState(''); + + const resetForm = () => { + setMode(EMPTY_CONFIG.mode); + setApiKey(''); + }; + + useEffect(() => { + if (!open) return; + const config = initialConfig ?? EMPTY_CONFIG; + setMode(config.mode); + setApiKey(''); + }, [open, initialConfig]); + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) resetForm(); + onOpenChange(nextOpen); + }; + + const trimmedKey = apiKey.trim(); + const isValid = !requireApiKey || !!trimmedKey; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (!isValid || busy) return; + + try { + await onSave({ + mode, + apiKey: trimmedKey, + }); + resetForm(); + onOpenChange(false); + } catch { + // Parent surfaces error; keep form open. + } + }; + + return ( + + + + } + > +
+
+
+ + { + setApiKey(event.target.value); + }} + placeholder={requireApiKey ? 'Parallel API key' : 'Leave blank to keep existing'} + autoFocus + className={inputClassName} + /> +
+ +
+ + +
+
+ +
+ {error ?

{error}

: null} + + Save + +
+
+
+ ); +}; + +export default ConfigureWebSearchForm; diff --git a/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx b/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx new file mode 100644 index 000000000..8a94e53b7 --- /dev/null +++ b/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { Button } from '../../atoms/primitives/Button.js'; +import { Icon } from '../../icons/Icon.js'; +import { useCatalogServer } from '../../server/ServerContext.js'; +import type { + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, +} from '../../server/types.js'; +import { getErrorMessage } from '../../utils/getErrorMessage.js'; +import { useToasterOptional } from '../ToasterContainer.js'; +import ConfigureWebSearchForm, { type WebSearchConfigDraft } from './ConfigureWebSearchForm.js'; + +const configFrom = ({ mode }: WebSearchProviderConfig): WebSearchProviderConfig => ({ mode }); + +const modeLabel = (mode: WebSearchProviderConfig['mode']): string => { + switch (mode) { + case 'turbo': + return 'Turbo'; + case 'fast': + return 'Fast'; + case 'basic': + return 'Basic'; + case 'advanced': + return 'Advanced'; + default: + return mode; + } +}; + +const WebSearchSettings = () => { + const { webSearchCatalog } = useCatalogServer(); + const toaster = useToasterOptional(); + + const [providers, setProviders] = useState([]); + const [catalog, setCatalog] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [formError, setFormError] = useState(null); + const [busy, setBusy] = useState(false); + + const [createEntry, setCreateEntry] = useState(null); + const [updateProvider, setUpdateProvider] = useState(null); + + const refresh = useCallback( + async ({ quiet = false }: { quiet?: boolean } = {}) => { + if (!webSearchCatalog) return; + if (!quiet) setLoading(true); + setError(null); + try { + const [listed, available] = await Promise.all([ + webSearchCatalog.listWebSearchProviders(), + webSearchCatalog.getWebSearchProviderCatalog(), + ]); + setProviders(listed); + setCatalog(available); + } catch (err) { + setError(getErrorMessage(err, 'Failed to load web search providers')); + } finally { + if (!quiet) setLoading(false); + } + }, + [webSearchCatalog], + ); + + useEffect(() => { + void refresh(); + }, [refresh]); + + // Tenant/UI-wide: only one web-search provider may be configured at a time. + const hasConfiguredProvider = providers.length > 0; + const availableEntries = useMemo(() => { + if (hasConfiguredProvider) return []; + const connectedCatalogIds = new Set(providers.map(provider => provider.catalogId)); + return catalog.filter(entry => !connectedCatalogIds.has(entry.id)); + }, [catalog, providers, hasConfiguredProvider]); + + const formInitialConfig = useMemo( + () => (updateProvider ? configFrom(updateProvider) : createEntry ? configFrom(createEntry) : null), + [updateProvider, createEntry], + ); + + if (!webSearchCatalog) { + return

Web search provider catalog is not available.

; + } + + const runMutation = async (fn: () => Promise, setMutationError = setError) => { + setBusy(true); + setError(null); + try { + await fn(); + await refresh({ quiet: true }); + } catch (err) { + setMutationError(getErrorMessage(err, 'Request failed')); + throw err; + } finally { + setBusy(false); + } + }; + + const handleCreate = async (draft: WebSearchConfigDraft) => { + if (!createEntry) return; + setFormError(null); + await runMutation(async () => { + await webSearchCatalog.createWebSearchProvider({ + catalogId: createEntry.id, + name: createEntry.name, + type: createEntry.type, + ...configFrom(draft), + apiKey: draft.apiKey, + }); + }, setFormError); + setCreateEntry(null); + setTimeout(() => { + toaster?.showSuccess({ title: `${createEntry.name} configured` }); + }, 0); + }; + + const handleUpdate = async (draft: WebSearchConfigDraft) => { + if (!updateProvider) return; + setFormError(null); + await runMutation(async () => { + await webSearchCatalog.updateWebSearchProvider({ + id: updateProvider.id, + ...configFrom(draft), + ...(draft.apiKey ? { apiKey: draft.apiKey } : {}), + }); + }, setFormError); + setUpdateProvider(null); + setTimeout(() => { + toaster?.showSuccess({ title: `${updateProvider.name} updated` }); + }, 0); + }; + + const formOpen = createEntry != null || updateProvider != null; + const isUpdate = updateProvider != null; + const formTitle = updateProvider + ? `Update ${updateProvider.name}` + : createEntry + ? `Configure ${createEntry.name}` + : 'Configure web search provider'; + + return ( + <> +

Web search

+

+ Configure a provider so agents can search the web. Only one can be active at a time. +

+ + {error ? ( +

+ {error} +

+ ) : null} + +
+ {loading ? ( +

Loading web search providers…

+ ) : ( +
+ {providers.length > 0 ? ( +
+

+ Configured · {providers.length} +

+
+ {providers.map(provider => ( +
+
+ + + +
+
{provider.name}
+

Mode · {modeLabel(provider.mode)}

+
+
+ +
+ + + Connected + + { + setFormError(null); + setCreateEntry(null); + setUpdateProvider(provider); + }} + > + Update + +
+
+ ))} +
+
+ ) : null} + + {!hasConfiguredProvider ? ( +
+

+ Available · {availableEntries.length} +

+ {availableEntries.length === 0 ? ( +

+ {catalog.length > 0 + ? 'All catalog providers are configured.' + : 'No web search providers in the catalog.'} +

+ ) : ( +
+ {availableEntries.map(entry => ( +
+
+ + + +
+
{entry.name}
+

Mode · {modeLabel(entry.mode)}

+
+
+ + { + setFormError(null); + setUpdateProvider(null); + setCreateEntry(entry); + }} + > + Configure + +
+ ))} +
+ )} +
+ ) : null} +
+ )} +
+ + { + if (!open) { + setFormError(null); + setCreateEntry(null); + setUpdateProvider(null); + } + }} + onSave={isUpdate ? handleUpdate : handleCreate} + title={formTitle} + description={ + isUpdate + ? 'Update this web search provider. Leave API key blank to keep the existing key.' + : 'Configure this web search provider. API key is never stored in the catalog.' + } + initialConfig={formInitialConfig} + requireApiKey={!isUpdate} + busy={busy} + error={formError} + /> + + ); +}; + +export default WebSearchSettings; diff --git a/packages/trueforge-ui/src/containers/SettingsBuilder/index.tsx b/packages/trueforge-ui/src/containers/SettingsBuilder/index.tsx index 4f1736a1f..cdb23c64f 100644 --- a/packages/trueforge-ui/src/containers/SettingsBuilder/index.tsx +++ b/packages/trueforge-ui/src/containers/SettingsBuilder/index.tsx @@ -17,6 +17,7 @@ const ModelSettings = lazy(() => import('./ModelSettings.js')); const ConnectorSettings = lazy(() => import('./ConnectorSettings.js')); const SkillSettings = lazy(() => import('./SkillSettings.js')); const SandboxSettings = lazy(() => import('./SandboxSettings.js')); +const WebSearchSettings = lazy(() => import('./WebSearchSettings.js')); function SettingsSectionFallback() { return ( @@ -36,6 +37,7 @@ const TruefoundrySettingsBuilder = () => { const compact = useCompactLayout(); const hasSkills = catalog?.skillCatalog != null; const hasSandbox = catalog?.sandboxCatalog != null; + const hasWebSearch = catalog?.webSearchCatalog != null; const closeSettings = useCallback(() => { setSettingsOpen(false); @@ -57,7 +59,10 @@ const TruefoundrySettingsBuilder = () => { if (!hasSandbox && section === 'sandbox') { setSettingsOpen(settingsOpen, 'models'); } - }, [hasSkills, hasSandbox, section, settingsOpen, setSettingsOpen]); + if (!hasWebSearch && section === 'web-search') { + setSettingsOpen(settingsOpen, 'models'); + } + }, [hasSkills, hasSandbox, hasWebSearch, section, settingsOpen, setSettingsOpen]); useEffect(() => { if (!settingsOpen) return; @@ -75,13 +80,13 @@ const TruefoundrySettingsBuilder = () => { Array<{ id: SettingsSection; label: string; - icon: 'cpu' | 'plug' | 'lightbulb' | 'cube'; + icon: 'cpu' | 'plug' | 'lightbulb' | 'cube' | 'search'; }> >(() => { const baseSections: Array<{ id: SettingsSection; label: string; - icon: 'cpu' | 'plug' | 'lightbulb' | 'cube'; + icon: 'cpu' | 'plug' | 'lightbulb' | 'cube' | 'search'; }> = [ { id: 'models', label: 'Models', icon: 'cpu' }, { id: 'connectors', label: 'Connectors', icon: 'plug' }, @@ -92,8 +97,11 @@ const TruefoundrySettingsBuilder = () => { if (hasSandbox) { baseSections.push({ id: 'sandbox', label: 'Sandbox providers', icon: 'cube' }); } + if (hasWebSearch) { + baseSections.push({ id: 'web-search', label: 'Web search', icon: 'search' }); + } return baseSections; - }, [hasSkills, hasSandbox]); + }, [hasSkills, hasSandbox, hasWebSearch]); if (!settingsOpen || !catalog) return null; @@ -154,6 +162,7 @@ const TruefoundrySettingsBuilder = () => { {section === 'connectors' ? : null} {section === 'skills' && hasSkills ? : null} {section === 'sandbox' && hasSandbox ? : null} + {section === 'web-search' && hasWebSearch ? : null} diff --git a/packages/trueforge-ui/src/index.ts b/packages/trueforge-ui/src/index.ts index 651daed6a..d5cab5c1e 100644 --- a/packages/trueforge-ui/src/index.ts +++ b/packages/trueforge-ui/src/index.ts @@ -371,6 +371,7 @@ export type { CreateSessionRequest, CreateSkillRequest, CreateSkillRequestBase, + CreateWebSearchProviderRequest, CreatedBySubject, DefinedSkill, GithubSkill, @@ -393,6 +394,7 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, + ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -430,10 +432,15 @@ export type { UpdateModelProviderRequest, UpdateSandboxProviderRequest, UpdateSessionRequest, + UpdateWebSearchProviderRequest, UserMessage, UserMessageContent, UserToolApprovalEvent, UserToolResponseEvent, + WebSearchCatalogServer, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, } from './server/types.js'; export { AgentCodeBlock } from './atoms/agent-details/AgentCodeBlock.js'; diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts new file mode 100644 index 000000000..60fd91937 --- /dev/null +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts @@ -0,0 +1,145 @@ +/** + * Maps trueforge-ui web-search-settings calls onto Harness + * `/api/v1/settings/web-search-providers` (singleton upsert, no delete). + * + * UI: multi-row providers with `id` / `catalogId` / `name` / flat `apiKey`. + * Harness: one Parallel provider per tenant; catalog YAML has no name — synthetic + * identity uses `type` (`parallel`) as id/catalogId and display name `Parallel`. + */ +import type { TrueForge } from '@truefoundry/trueforge-sdk'; +import { TrueForgeApi } from '@truefoundry/trueforge-sdk'; +import type { + WebSearchCatalogServer, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, +} from '../../../server/types.js'; + +export type UiWebSearchProvider = WebSearchProviderBase; +export type UiWebSearchProviderCatalogEntry = WebSearchProviderCatalogEntry; + +const PARALLEL_TYPE = 'parallel'; +const PARALLEL_DISPLAY_NAME = 'Parallel'; + +function displayNameForType(type: string): string { + if (type === PARALLEL_TYPE) { + return PARALLEL_DISPLAY_NAME; + } + return type; +} + +export function configFromHarness( + provider: TrueForgeApi.CatalogWebSearchProvider | TrueForgeApi.WebSearchProviderManifest, +): WebSearchProviderConfig { + return { + mode: provider.mode, + }; +} + +export function toUiCatalogEntry(provider: TrueForgeApi.CatalogWebSearchProvider): UiWebSearchProviderCatalogEntry { + return { + id: provider.type, + name: displayNameForType(provider.type), + type: provider.type, + ...configFromHarness(provider), + }; +} + +export function toUiWebSearchProvider(provider: TrueForgeApi.WebSearchProviderManifest): UiWebSearchProvider { + return { + id: provider.type, + name: displayNameForType(provider.type), + catalogId: provider.type, + isConnected: true, + ...configFromHarness(provider), + }; +} + +export function filterUiWebSearchProviders({ + providers, + query, +}: { + providers: UiWebSearchProvider[]; + query?: string; +}): UiWebSearchProvider[] { + const normalizedQuery = query?.trim().toLowerCase(); + if (normalizedQuery === undefined || normalizedQuery === '') { + return providers; + } + return providers.filter( + provider => + provider.name.toLowerCase().includes(normalizedQuery) || provider.id.toLowerCase().includes(normalizedQuery), + ); +} + +export function toHarnessManifest( + req: { + type: string; + apiKey: string; + } & WebSearchProviderConfig, +): TrueForgeApi.WebSearchProviderManifest { + if (req.type !== PARALLEL_TYPE) { + throw new Error(`Unsupported web-search provider type: ${req.type}`); + } + return { + type: PARALLEL_TYPE, + mode: req.mode, + auth: { apiKey: req.apiKey }, + }; +} + +function findExistingProvider( + providers: TrueForgeApi.ConfiguredWebSearchProvider[], + id: string, +): TrueForgeApi.ConfiguredWebSearchProvider | undefined { + return providers.find(provider => provider.name === id || provider.manifest.type === id); +} + +/** Settings web-search-catalog port for `createTrueForgeServer`. Delete omitted (no BE route). */ +export function createWebSearchProviderCatalog(client: TrueForge): WebSearchCatalogServer { + async function resolveApiKey({ apiKey, id }: { apiKey: string | undefined; id: string }): Promise { + const trimmed = apiKey?.trim(); + if (trimmed !== undefined && trimmed !== '') { + return trimmed; + } + const existing = await client.settings.webSearchProviders.list(); + const match = findExistingProvider(existing.data, id); + if (match === undefined) { + throw new Error('API key is required'); + } + return match.manifest.auth.apiKey; + } + + return { + getWebSearchProviderCatalog: async () => { + const body = await client.catalogs.webSearchProviders.list(); + return body.data.map(toUiCatalogEntry); + }, + listWebSearchProviders: async req => { + const body = await client.settings.webSearchProviders.list(); + const providers = body.data.map(entry => toUiWebSearchProvider(entry.manifest)); + return filterUiWebSearchProviders({ providers, query: req?.query }); + }, + createWebSearchProvider: async req => { + const body = await client.settings.webSearchProviders.create({ + manifest: toHarnessManifest({ + type: req.type, + apiKey: req.apiKey, + mode: req.mode, + }), + }); + return toUiWebSearchProvider(body.data.manifest); + }, + updateWebSearchProvider: async req => { + const apiKey = await resolveApiKey({ apiKey: req.apiKey, id: req.id }); + const body = await client.settings.webSearchProviders.createOrUpdate({ + manifest: toHarnessManifest({ + type: req.id, + apiKey, + mode: req.mode, + }), + }); + return toUiWebSearchProvider(body.data.manifest); + }, + }; +} diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts index d07861aa2..4625ec2fc 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts @@ -10,6 +10,7 @@ import { createConnectorCatalog } from './catalogs/connectorCatalog.js'; import { createModelProviderCatalog } from './catalogs/modelProviderCatalog.js'; import { createSandboxProviderCatalog } from './catalogs/sandboxProviderCatalog.js'; import { createSkillCatalog } from './catalogs/skillCatalog.js'; +import { createWebSearchProviderCatalog } from './catalogs/webSearchProviderCatalog.js'; import { createHarnessChatServer } from './chatServer.js'; import { createTrueForgeClient, type CreateTrueForgeClientOptions } from './client.js'; import { createHarnessPermissionsServer } from './permissionsServer.js'; @@ -49,6 +50,14 @@ export { toUiSandboxProviderListEntry, } from './catalogs/sandboxProviderCatalog.js'; export { createSkillCatalog, toHarnessManifest as toHarnessSkillManifest, toUiSkill } from './catalogs/skillCatalog.js'; +export { + createWebSearchProviderCatalog, + filterUiWebSearchProviders, + toHarnessManifest as toHarnessWebSearchManifest, + toUiCatalogEntry as toUiWebSearchCatalogEntry, + toUiWebSearchProvider, + configFromHarness as webSearchConfigFromHarness, +} from './catalogs/webSearchProviderCatalog.js'; export { createHarnessChatServer, toHarnessAgentSpec, @@ -82,6 +91,7 @@ export function createTrueForgeAgentUIServer(options: CreateTrueForgeAgentUIServ connectorCatalog: createConnectorCatalog(client), skillCatalog: createSkillCatalog(client), sandboxCatalog: createSandboxProviderCatalog(client), + webSearchCatalog: createWebSearchProviderCatalog(client), } satisfies CatalogServer); return createTrueForgeServer({ diff --git a/packages/trueforge-ui/src/server/ShellModeContext.tsx b/packages/trueforge-ui/src/server/ShellModeContext.tsx index ad5c41b2c..95ce0a133 100644 --- a/packages/trueforge-ui/src/server/ShellModeContext.tsx +++ b/packages/trueforge-ui/src/server/ShellModeContext.tsx @@ -71,7 +71,7 @@ export type SelectLibraryAgentRequest = { agentSpec?: AgentSpec; }; -export type SettingsSection = 'models' | 'connectors' | 'skills' | 'sandbox'; +export type SettingsSection = 'models' | 'connectors' | 'skills' | 'sandbox' | 'web-search'; export type HistoryAgentFilter = { agentId?: string; diff --git a/packages/trueforge-ui/src/server/types.ts b/packages/trueforge-ui/src/server/types.ts index f7c7dc6ea..06b7957ec 100644 --- a/packages/trueforge-ui/src/server/types.ts +++ b/packages/trueforge-ui/src/server/types.ts @@ -59,6 +59,7 @@ export type { CreateSessionRequest, CreateSkillRequest, CreateSkillRequestBase, + CreateWebSearchProviderRequest, CreatedBySubject, DefinedSkill, GithubSkill, @@ -83,6 +84,7 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, + ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -126,8 +128,16 @@ export type { UpdateSandboxProviderRequest, UpdateScheduleRequest, UpdateSessionRequest, + UpdateWebSearchProviderRequest, UserMessage, UserMessageContent, UserToolApprovalEvent, UserToolResponseEvent, + WebSearchBase, + WebSearchCatalogEntry, + WebSearchCatalogServer, + WebSearchConfig, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, + WebSearchProviderConfig, } from '@truefoundry/trueforge-assistant-ui-runtime/server'; diff --git a/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx b/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx new file mode 100644 index 000000000..de3c55924 --- /dev/null +++ b/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx @@ -0,0 +1,187 @@ +// @vitest-environment jsdom +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import WebSearchSettings from '@/containers/SettingsBuilder/WebSearchSettings.js'; +import { ServerProvider } from '@/server/ServerContext.js'; +import type { + CreateWebSearchProviderRequest, + UpdateWebSearchProviderRequest, + WebSearchProviderBase, + WebSearchProviderCatalogEntry, +} from '@/server/types.js'; +import { createMockAgentUIServer, createMockCatalog } from '../../server/mockServer.js'; + +beforeAll(() => { + HTMLDialogElement.prototype.showModal = function showModal() { + this.setAttribute('open', ''); + }; + HTMLDialogElement.prototype.close = function close() { + this.removeAttribute('open'); + }; +}); + +const catalogEntry: WebSearchProviderCatalogEntry = { + id: 'parallel', + name: 'Parallel', + type: 'parallel', + mode: 'turbo', +}; + +function createFakeHost(initial: WebSearchProviderBase[] = []) { + let providers = [...initial]; + const created: CreateWebSearchProviderRequest[] = []; + const updated: UpdateWebSearchProviderRequest[] = []; + + const webSearchCatalog = { + getWebSearchProviderCatalog: async () => [catalogEntry], + listWebSearchProviders: async () => providers, + createWebSearchProvider: async (req: CreateWebSearchProviderRequest) => { + created.push(req); + const provider: WebSearchProviderBase = { + id: req.catalogId, + name: req.name, + catalogId: req.catalogId, + isConnected: true, + mode: req.mode, + }; + providers = [...providers, provider]; + return provider; + }, + updateWebSearchProvider: async (req: UpdateWebSearchProviderRequest) => { + updated.push(req); + providers = providers.map(provider => + provider.id === req.id + ? { + ...provider, + mode: req.mode, + } + : provider, + ); + const next = providers.find(provider => provider.id === req.id); + if (next === undefined) { + throw new Error(`Web search provider "${req.id}" not found`); + } + return next; + }, + }; + + const server = createMockAgentUIServer({ + catalog: createMockCatalog({ webSearchCatalog }), + }); + + return { + created, + updated, + wrapper: ({ children }: { children: ReactNode }) => {children}, + }; +} + +describe('WebSearchSettings', () => { + it('autofills create form from catalog except apiKey', async () => { + const host = createFakeHost(); + const { wrapper: Wrapper } = host; + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText('Parallel')).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Configure' })); + + await waitFor(() => { + expect(screen.getByLabelText('Mode')).toBeTruthy(); + }); + expect(screen.getByLabelText('Mode')).toHaveProperty('value', 'turbo'); + expect(screen.getByLabelText('API key')).toHaveProperty('value', ''); + + fireEvent.change(screen.getByLabelText('API key'), { + target: { value: 'par_secret' }, + }); + fireEvent.change(screen.getByLabelText('Mode'), { + target: { value: 'fast' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + + await waitFor(() => { + expect(host.created).toHaveLength(1); + }); + expect(host.created[0]).toMatchObject({ + catalogId: 'parallel', + name: 'Parallel', + type: 'parallel', + mode: 'fast', + apiKey: 'par_secret', + }); + }); + + it('prefills update form and allows saving without re-entering apiKey', async () => { + const existing: WebSearchProviderBase = { + id: 'parallel', + name: 'Parallel', + catalogId: 'parallel', + isConnected: true, + mode: 'basic', + }; + const host = createFakeHost([existing]); + const { wrapper: Wrapper } = host; + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Update' })).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Update' })); + + await waitFor(() => { + expect(screen.getByLabelText('Mode')).toHaveProperty('value', 'basic'); + }); + expect(screen.getByLabelText(/API key/)).toHaveProperty('value', ''); + + fireEvent.change(screen.getByLabelText('Mode'), { + target: { value: 'advanced' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + + await waitFor(() => { + expect(host.updated).toHaveLength(1); + }); + expect(host.updated[0]).toEqual({ + id: 'parallel', + mode: 'advanced', + }); + expect(host.updated[0]).not.toHaveProperty('apiKey'); + }); + + it('hides other catalog providers once one is configured', async () => { + const existing: WebSearchProviderBase = { + id: 'parallel', + name: 'Parallel', + catalogId: 'parallel', + isConnected: true, + mode: 'turbo', + }; + const host = createFakeHost([existing]); + const { wrapper: Wrapper } = host; + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText('Web search')).toBeTruthy(); + }); + expect(screen.queryByRole('button', { name: 'Configure' })).toBeNull(); + expect(screen.queryByText(/^Available ·/)).toBeNull(); + }); +}); diff --git a/packages/trueforge-ui/test/containers/SettingsBuilder/index.test.tsx b/packages/trueforge-ui/test/containers/SettingsBuilder/index.test.tsx index 572721636..7474b4a3d 100644 --- a/packages/trueforge-ui/test/containers/SettingsBuilder/index.test.tsx +++ b/packages/trueforge-ui/test/containers/SettingsBuilder/index.test.tsx @@ -7,7 +7,12 @@ import { DraftCatalogProvider } from '@/atoms/draft/DraftCatalogProvider.js'; import TruefoundrySettingsBuilder from '@/containers/SettingsBuilder/index.js'; import { ServerProvider } from '@/server/ServerContext.js'; import { ShellModeProvider, useShellMode } from '@/server/ShellModeContext.js'; -import type { AgentUIServer, SandboxCatalogServer, SkillCatalogServer } from '@/server/types.js'; +import type { + AgentUIServer, + SandboxCatalogServer, + SkillCatalogServer, + WebSearchCatalogServer, +} from '@/server/types.js'; import { createMockAgentUIServer, createMockCatalog } from '../../server/mockServer.js'; vi.mock('@/containers/SettingsBuilder/ModelSettings.js', () => ({ @@ -26,6 +31,10 @@ vi.mock('@/containers/SettingsBuilder/SandboxSettings.js', () => ({ default: () =>
Sandbox settings content
, })); +vi.mock('@/containers/SettingsBuilder/WebSearchSettings.js', () => ({ + default: () =>
Web search settings content
, +})); + async function unavailable(): Promise { throw new Error('Unexpected settings section call'); } @@ -43,7 +52,16 @@ const sandboxCatalog: SandboxCatalogServer = { updateSandboxProvider: unavailable, }; -function createServer(options: { catalog?: boolean; skills?: boolean; sandbox?: boolean } = {}): AgentUIServer { +const webSearchCatalog: WebSearchCatalogServer = { + getWebSearchProviderCatalog: async () => [], + listWebSearchProviders: async () => [], + createWebSearchProvider: unavailable, + updateWebSearchProvider: unavailable, +}; + +function createServer( + options: { catalog?: boolean; skills?: boolean; sandbox?: boolean; webSearch?: boolean } = {}, +): AgentUIServer { if (options.catalog === false) { return createMockAgentUIServer(); } @@ -51,6 +69,7 @@ function createServer(options: { catalog?: boolean; skills?: boolean; sandbox?: catalog: createMockCatalog({ ...(options.skills ? { skillCatalog } : {}), ...(options.sandbox ? { sandboxCatalog } : {}), + ...(options.webSearch ? { webSearchCatalog } : {}), }), }); } @@ -113,11 +132,12 @@ describe('TruefoundrySettingsBuilder', () => { expect(screen.getByRole('button', { name: 'Connectors' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Skills' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Sandbox' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Web search' })).not.toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Model settings content')).toBeInTheDocument(); }); - rerender(); + rerender(); fireEvent.click(await screen.findByRole('button', { name: 'Skills' })); await waitFor(() => { @@ -129,6 +149,11 @@ describe('TruefoundrySettingsBuilder', () => { expect(screen.getByText('Sandbox settings content')).toBeInTheDocument(); }); + fireEvent.click(screen.getByRole('button', { name: 'Web search' })); + await waitFor(() => { + expect(screen.getByText('Web search settings content')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByRole('button', { name: 'Connectors' })); await waitFor(() => { expect(screen.getByText('Connector settings content')).toBeInTheDocument(); diff --git a/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts new file mode 100644 index 000000000..0c0a13113 --- /dev/null +++ b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'vitest'; + +import { + configFromHarness, + filterUiWebSearchProviders, + toHarnessManifest, + toUiCatalogEntry, + toUiWebSearchProvider, +} from '@/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.js'; + +describe('webSearchProviderCatalog mappers', () => { + const harnessCatalog = { + type: 'parallel' as const, + mode: 'turbo' as const, + }; + + const harnessConfigured = { + ...harnessCatalog, + auth: { apiKey: 'par_secret' }, + }; + + it('stamps catalog identity from type and strips auth', () => { + assert.deepEqual(toUiCatalogEntry(harnessCatalog), { + id: 'parallel', + name: 'Parallel', + type: 'parallel', + mode: 'turbo', + }); + }); + + it('maps configured provider without embedding apiKey', () => { + assert.deepEqual(toUiWebSearchProvider(harnessConfigured), { + id: 'parallel', + name: 'Parallel', + catalogId: 'parallel', + isConnected: true, + mode: 'turbo', + }); + assert.equal('auth' in toUiWebSearchProvider(harnessConfigured), false); + assert.equal('apiKey' in toUiWebSearchProvider(harnessConfigured), false); + }); + + it('filters providers by provider identity', () => { + const provider = toUiWebSearchProvider(harnessConfigured); + + assert.deepEqual(filterUiWebSearchProviders({ providers: [provider], query: ' PAR ' }), [provider]); + assert.deepEqual(filterUiWebSearchProviders({ providers: [provider], query: 'missing' }), []); + }); + + it('round-trips config fields into harness upsert body', () => { + assert.deepEqual( + toHarnessManifest({ + type: 'parallel', + apiKey: 'par_secret', + ...configFromHarness(harnessCatalog), + }), + harnessConfigured, + ); + }); + + it('rejects unsupported web-search provider types', () => { + assert.throws( + () => + toHarnessManifest({ + type: 'other', + apiKey: 'x', + mode: 'turbo', + }), + /Unsupported web-search provider type/, + ); + }); +}); From 30c5265d63e0d3ecc4340e7999791c1425db1065 Mon Sep 17 00:00:00 2001 From: thesujai Date: Tue, 22 Sep 2026 16:02:42 +0530 Subject: [PATCH 06/10] Refactor web search provider API and configuration --- .../web-search/ParallelWebSearchProvider.ts | 8 +- .../trueforge/catalog/web-search-catalog.yaml | 1 - packages/trueforge/src/apis/settings.ts | 1 - .../trueforge/src/apis/webSearchProviders.ts | 78 ++++++----------- .../src/catalog/webSearchCatalog.gen.ts | 2 +- .../20260921_000001_web_search_provider.ts | 9 +- packages/trueforge/src/db/postgres/types.ts | 5 +- .../PostgresWebSearchProviderStore.ts | 80 ++--------------- .../20260921_000001_web_search_provider.ts | 7 +- packages/trueforge/src/db/sqlite/types.ts | 5 +- .../SqliteWebSearchProviderStore.ts | 78 ++--------------- .../src/db/webSearchProviderStore.ts | 46 +--------- .../trueforge/src/routes/catalogRoutes.ts | 7 +- packages/trueforge/src/routes/openapiTags.ts | 2 +- .../src/routes/webSearchProviderRoutes.ts | 65 +++----------- .../trueforge/src/schemas/webSearchCatalog.ts | 1 - .../src/schemas/webSearchProvider.ts | 28 +----- .../TrueFoundryWebSearchProviderStore.ts | 48 ++--------- packages/trueforge/src/websearch/providers.ts | 10 +-- .../tests/unit/apis/capabilities.test.ts | 38 +++++++-- .../trueforge/tests/unit/apis/turns.test.ts | 2 +- .../unit/apis/webSearchProviders.test.ts | 85 +++++++++++++++++++ 22 files changed, 217 insertions(+), 389 deletions(-) create mode 100644 packages/trueforge/tests/unit/apis/webSearchProviders.test.ts diff --git a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts index 5e1846ac9..85cb47a08 100644 --- a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts +++ b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts @@ -15,7 +15,8 @@ const PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500; const PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000; export interface ParallelWebSearchProviderOptions { - apiKey: string; + /** Parallel API key; omit or empty when the account allows unauthenticated use. */ + apiKey?: string; /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */ mode: ParallelSearchMode; } @@ -29,7 +30,10 @@ export class ParallelWebSearchProvider implements IWebSearchProvider { readonly #mode: ParallelSearchMode; constructor(options: ParallelWebSearchProviderOptions) { - this.#client = new Parallel({ apiKey: options.apiKey, fetch: ssrfFetch }); + this.#client = new Parallel({ + apiKey: options.apiKey ?? '', + fetch: ssrfFetch, + }); this.#mode = options.mode; } diff --git a/packages/trueforge/catalog/web-search-catalog.yaml b/packages/trueforge/catalog/web-search-catalog.yaml index bb319b3f9..a6d34646b 100644 --- a/packages/trueforge/catalog/web-search-catalog.yaml +++ b/packages/trueforge/catalog/web-search-catalog.yaml @@ -1,3 +1,2 @@ providers: - type: parallel - mode: turbo diff --git a/packages/trueforge/src/apis/settings.ts b/packages/trueforge/src/apis/settings.ts index ab77761d7..e6e9e4667 100644 --- a/packages/trueforge/src/apis/settings.ts +++ b/packages/trueforge/src/apis/settings.ts @@ -72,7 +72,6 @@ export function createSettingsRouter(deps: SettingsRouterDeps { - resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; - withTransaction: WithTransaction; +export interface WebSearchProvidersRouterDeps { + resolveWebSearchProviderStore: (c: Context) => IWebSearchProviderStore; resolveRequestContext: ResolveRequestContext; } function redactWebSearchProvider(manifest: WebSearchProviderManifest): WebSearchProviderManifest { + if (!manifest.auth) { + return manifest; + } return { ...manifest, auth: { api_key: toRedactedSecretValue(manifest.auth.api_key) }, @@ -41,12 +33,15 @@ function resolveWebSearchProviderManifestForWrite({ incoming: WebSearchProviderManifest; existing: WebSearchProviderManifest | undefined; }): WebSearchProviderManifest { + if (!incoming.auth) { + return incoming; + } return { ...incoming, auth: { api_key: resolveStoredSecretValue({ incoming: incoming.auth.api_key, - existing: existing?.auth.api_key, + existing: existing?.auth?.api_key, }), }, }; @@ -54,40 +49,19 @@ function resolveWebSearchProviderManifestForWrite({ function toWireProvider(record: WebSearchProviderRecord): ConfiguredWebSearchProvider { return { - name: record.name, + name: webSearchProviderName(record.manifest), manifest: redactWebSearchProvider(record.manifest), }; } -export function createWebSearchProvidersRouter(deps: WebSearchProvidersRouterDeps) { - const listHandler: RouteHandler = async c => { +export function createWebSearchProvidersRouter(deps: WebSearchProvidersRouterDeps) { + const getHandler: RouteHandler = async c => { const requestContext = deps.resolveRequestContext(c); - const records = await deps.resolveWebSearchProviderStore(c).listProviders({ tenant_id: requestContext.tenant_id }); - return c.json({ data: records.map(toWireProvider) }, 200); - }; - - const createHandler: RouteHandler = async c => { - const body: CreateWebSearchProviderRequest = c.req.valid('json'); - const requestContext = deps.resolveRequestContext(c); - const provider = body.manifest; - const name = webSearchProviderName(provider); - try { - const manifest = resolveWebSearchProviderManifestForWrite({ incoming: provider, existing: undefined }); - const record = await deps.resolveWebSearchProviderStore(c).createProvider({ - tenant_id: requestContext.tenant_id, - name, - manifest, - }); - return c.json({ data: toWireProvider(record) }, 201); - } catch (error) { - if (error instanceof MissingStoredSecretError) { - return c.json({ error: { message: 'API key is required' } }, 400); - } - if (error instanceof WebSearchProviderNameConflictError) { - return c.json({ error: { message: error.message } }, 409); - } - throw error; + const record = await deps.resolveWebSearchProviderStore(c).getProvider(requestContext.tenant_id); + if (!record) { + return c.json({ error: { message: 'No web search provider configured' } }, 404); } + return c.json({ data: toWireProvider(record) }, 200); }; const putHandler: RouteHandler = async c => { @@ -95,16 +69,13 @@ export function createWebSearchProvidersRouter(deps: WebSearchProv const body: UpdateWebSearchProviderRequest = c.req.valid('json'); const requestContext = deps.resolveRequestContext(c); const provider = body.manifest; - const name = webSearchProviderName(provider); try { - const record = await deps.withTransaction(async transaction => { - const existing = await store.getProviderForUpdate({ tenant_id: requestContext.tenant_id, name }, transaction); - const manifest = resolveWebSearchProviderManifestForWrite({ - incoming: provider, - existing: existing?.manifest, - }); - return store.upsertProvider({ tenant_id: requestContext.tenant_id, name, manifest }, transaction); + const existing = await store.getProvider(requestContext.tenant_id); + const manifest = resolveWebSearchProviderManifestForWrite({ + incoming: provider, + existing: existing?.manifest, }); + const record = await store.upsertProvider({ tenant_id: requestContext.tenant_id, manifest }); return c.json({ data: toWireProvider(record) }, 200); } catch (error) { if (error instanceof MissingStoredSecretError) { @@ -115,8 +86,7 @@ export function createWebSearchProvidersRouter(deps: WebSearchProv }; const router = new OpenAPIHono(); - router.openapi(listWebSearchProvidersRoute, listHandler); - router.openapi(createWebSearchProviderRoute, createHandler); + router.openapi(getWebSearchProviderRoute, getHandler); router.openapi(putWebSearchProviderRoute, putHandler); return router; } diff --git a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts index 04d5110ad..5af2a7658 100644 --- a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts +++ b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts @@ -1,2 +1,2 @@ // AUTO-GENERATED by scripts/generate-catalog.mjs — do not edit. -export const shippedWebSearchCatalogYaml = "providers:\n - type: parallel\n mode: turbo\n" as const; +export const shippedWebSearchCatalogYaml = 'providers:\n - type: parallel\n' as const; diff --git a/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts b/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts index f6e26efbf..b8c254328 100644 --- a/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts +++ b/packages/trueforge/src/db/postgres/migrations/20260921_000001_web_search_provider.ts @@ -1,15 +1,20 @@ import { sql, type Kysely } from 'kysely'; +/** + * Configured web-search provider (canonical DDL owner). + * + * Singleton per tenant: PK is `tenant_id` only. Manifest (type, auth) is one + * jsonb document validated by Zod on every write. + */ export async function up(db: Kysely): Promise { await sql`SET LOCAL lock_timeout = '5s'`.execute(db); await db.schema .createTable('web_search_provider') .addColumn('tenant_id', 'text', col => col.notNull()) - .addColumn('name', 'text', col => col.notNull()) .addColumn('manifest', 'jsonb', col => col.notNull()) .addColumn('created_at', 'timestamptz', col => col.notNull()) .addColumn('updated_at', 'timestamptz', col => col.notNull()) - .addPrimaryKeyConstraint('web_search_provider_pkey', ['tenant_id', 'name']) + .addPrimaryKeyConstraint('web_search_provider_pkey', ['tenant_id']) .execute(); } diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 584e3a64c..e9afee3fe 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -342,9 +342,12 @@ export interface ModelProviderTable { updated_at: Date; } +/** + * Configured web-search provider — mirrors the Postgres `web_search_provider` table. + * PRIMARY KEY (tenant_id) + */ export interface WebSearchProviderTable { tenant_id: string; - name: string; manifest: JSONColumnType; created_at: Date; updated_at: Date; diff --git a/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts b/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts index cf1b30494..adbb3f99a 100644 --- a/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts +++ b/packages/trueforge/src/db/postgres/web-search-provider-store/PostgresWebSearchProviderStore.ts @@ -1,22 +1,15 @@ import type { Kysely, Selectable, Transaction } from 'kysely'; -import { - WebSearchProviderNameConflictError, - type CreateWebSearchProviderInput, - type GetWebSearchProviderForUpdateInput, - type GetWebSearchProviderInput, - type IWebSearchProviderStore, - type ListWebSearchProvidersInput, - type UpsertWebSearchProviderInput, - type WebSearchProviderRecord, +import type { + IWebSearchProviderStore, + UpsertWebSearchProviderInput, + WebSearchProviderRecord, } from '../../webSearchProviderStore'; -import { isUniqueViolation } from '../client'; import { json, now } from '../sqlExpressions'; import type { Database, WebSearchProviderTable } from '../types'; function toRecord(row: Selectable): WebSearchProviderRecord { return { tenant_id: row.tenant_id, - name: row.name, manifest: row.manifest, created_at: row.created_at.toISOString(), updated_at: row.updated_at.toISOString(), @@ -30,77 +23,19 @@ export class PostgresWebSearchProviderStore implements IWebSearchProviderStore, - ): Promise { - const db = transaction ?? this.#db; - const rows = await db - .selectFrom('web_search_provider') - .selectAll() - .where('tenant_id', '=', input.tenant_id) - .orderBy('name') - .execute(); - return rows.map(toRecord); - } - async getProvider( - input: GetWebSearchProviderInput, + tenantId: string, transaction?: Transaction, ): Promise { const db = transaction ?? this.#db; const row = await db .selectFrom('web_search_provider') .selectAll() - .where('tenant_id', '=', input.tenant_id) - .where('name', '=', input.name) - .executeTakeFirst(); - return row ? toRecord(row) : undefined; - } - - async getProviderForUpdate( - input: GetWebSearchProviderForUpdateInput, - transaction: Transaction, - ): Promise { - const row = await transaction - .selectFrom('web_search_provider') - .selectAll() - .where('tenant_id', '=', input.tenant_id) - .where('name', '=', input.name) - .forUpdate() + .where('tenant_id', '=', tenantId) .executeTakeFirst(); return row ? toRecord(row) : undefined; } - async createProvider( - input: CreateWebSearchProviderInput, - transaction?: Transaction, - ): Promise { - const db = transaction ?? this.#db; - try { - const row = await db - .insertInto('web_search_provider') - .values({ - tenant_id: input.tenant_id, - name: input.name, - manifest: json(input.manifest), - created_at: now(), - updated_at: now(), - }) - .returningAll() - .executeTakeFirstOrThrow(); - return toRecord(row); - } catch (error) { - if (isUniqueViolation(error)) { - throw new WebSearchProviderNameConflictError( - { tenant_id: input.tenant_id, name: input.name }, - { cause: error }, - ); - } - throw error; - } - } - async upsertProvider( input: UpsertWebSearchProviderInput, transaction?: Transaction, @@ -110,13 +45,12 @@ export class PostgresWebSearchProviderStore implements IWebSearchProviderStore - oc.columns(['tenant_id', 'name']).doUpdateSet({ + oc.column('tenant_id').doUpdateSet({ manifest: json(input.manifest), updated_at: now(), }), diff --git a/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts b/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts index 8c9075a9a..cad8a581d 100644 --- a/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts +++ b/packages/trueforge/src/db/sqlite/migrations/20260921_000001_web_search_provider.ts @@ -1,15 +1,18 @@ import { sql, type Kysely } from 'kysely'; +/** + * Configured web-search provider — mirrors Postgres `web_search_provider`. + * Singleton per tenant: PK is `tenant_id` only. + */ export async function up(db: Kysely): Promise { await db.transaction().execute(async trx => { await sql` CREATE TABLE web_search_provider ( tenant_id TEXT NOT NULL, - name TEXT NOT NULL, manifest BLOB NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, - PRIMARY KEY (tenant_id, name) + PRIMARY KEY (tenant_id) ) STRICT `.execute(trx); }); diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 1a1534811..de10ff0be 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -188,9 +188,12 @@ export interface ModelProviderTable { updated_at: string; } +/** + * Configured web-search provider — mirrors the Postgres `web_search_provider` table. + * PRIMARY KEY (tenant_id) + */ export interface WebSearchProviderTable { tenant_id: string; - name: string; manifest: JsonbColumn; created_at: string; updated_at: string; diff --git a/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts b/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts index af3f5f3ce..179bbece2 100644 --- a/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts +++ b/packages/trueforge/src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore.ts @@ -1,16 +1,10 @@ import type { ExpressionBuilder, Kysely, Transaction } from 'kysely'; import type { WebSearchProviderManifest } from '../../../schemas/webSearchProvider'; -import { - WebSearchProviderNameConflictError, - type CreateWebSearchProviderInput, - type GetWebSearchProviderForUpdateInput, - type GetWebSearchProviderInput, - type IWebSearchProviderStore, - type ListWebSearchProvidersInput, - type UpsertWebSearchProviderInput, - type WebSearchProviderRecord, +import type { + IWebSearchProviderStore, + UpsertWebSearchProviderInput, + WebSearchProviderRecord, } from '../../webSearchProviderStore'; -import { isUniqueViolation } from '../client'; import { jsonbBind, jsonText, nowIso } from '../sqlExpressions'; import type { Database } from '../types'; @@ -18,7 +12,6 @@ import type { Database } from '../types'; function recordColumns(eb: ExpressionBuilder) { return [ 'tenant_id' as const, - 'name' as const, jsonText(eb.ref('manifest')).as('manifest'), 'created_at' as const, 'updated_at' as const, @@ -32,74 +25,18 @@ export class SqliteWebSearchProviderStore implements IWebSearchProviderStore, - ): Promise { - const db = transaction ?? this.#db; - return await db - .selectFrom('web_search_provider') - .select(recordColumns) - .where('tenant_id', '=', input.tenant_id) - .orderBy('name') - .execute(); - } - async getProvider( - input: GetWebSearchProviderInput, + tenantId: string, transaction?: Transaction, ): Promise { const db = transaction ?? this.#db; return await db .selectFrom('web_search_provider') .select(recordColumns) - .where('tenant_id', '=', input.tenant_id) - .where('name', '=', input.name) + .where('tenant_id', '=', tenantId) .executeTakeFirst(); } - /** SQLite has no FOR UPDATE; write txn serializes secret keep/rotate. */ - async getProviderForUpdate( - input: GetWebSearchProviderForUpdateInput, - transaction: Transaction, - ): Promise { - return await transaction - .selectFrom('web_search_provider') - .select(recordColumns) - .where('tenant_id', '=', input.tenant_id) - .where('name', '=', input.name) - .executeTakeFirst(); - } - - async createProvider( - input: CreateWebSearchProviderInput, - transaction?: Transaction, - ): Promise { - const db = transaction ?? this.#db; - const timestamp = nowIso(); - try { - return await db - .insertInto('web_search_provider') - .values({ - tenant_id: input.tenant_id, - name: input.name, - manifest: jsonbBind(input.manifest), - created_at: timestamp, - updated_at: timestamp, - }) - .returning(recordColumns) - .executeTakeFirstOrThrow(); - } catch (error) { - if (isUniqueViolation(error)) { - throw new WebSearchProviderNameConflictError( - { tenant_id: input.tenant_id, name: input.name }, - { cause: error }, - ); - } - throw error; - } - } - async upsertProvider( input: UpsertWebSearchProviderInput, transaction?: Transaction, @@ -110,13 +47,12 @@ export class SqliteWebSearchProviderStore implements IWebSearchProviderStore - oc.columns(['tenant_id', 'name']).doUpdateSet({ + oc.column('tenant_id').doUpdateSet({ manifest: jsonbBind(input.manifest), updated_at: timestamp, }), diff --git a/packages/trueforge/src/db/webSearchProviderStore.ts b/packages/trueforge/src/db/webSearchProviderStore.ts index f73c0dfef..57ca3c53f 100644 --- a/packages/trueforge/src/db/webSearchProviderStore.ts +++ b/packages/trueforge/src/db/webSearchProviderStore.ts @@ -1,59 +1,19 @@ -import type { ResourceName } from '../schemas/common'; import type { WebSearchProviderManifest } from '../schemas/webSearchProvider'; export interface WebSearchProviderRecord { tenant_id: string; - name: ResourceName; manifest: WebSearchProviderManifest; created_at: string; updated_at: string; } -export interface ListWebSearchProvidersInput { +export interface UpsertWebSearchProviderInput { tenant_id: string; -} - -export interface GetWebSearchProviderInput { - tenant_id: string; - name: string; -} - -export interface GetWebSearchProviderForUpdateInput { - tenant_id: string; - name: string; -} - -export interface CreateWebSearchProviderInput { - tenant_id: string; - name: ResourceName; manifest: WebSearchProviderManifest; } -export type UpsertWebSearchProviderInput = CreateWebSearchProviderInput; - -export class WebSearchProviderNameConflictError extends Error { - readonly tenant_id: string; - readonly provider_name: string; - - constructor({ tenant_id, name }: { tenant_id: string; name: string }, options?: ErrorOptions) { - super(`Web search provider name already exists: ${name}`, options); - this.name = 'WebSearchProviderNameConflictError'; - this.tenant_id = tenant_id; - this.provider_name = name; - } -} - export interface IWebSearchProviderStore { - listProviders(input: ListWebSearchProvidersInput, transaction?: TTransaction): Promise; - getProvider( - input: GetWebSearchProviderInput, - transaction?: TTransaction, - ): Promise; - /** Row lock for secret keep/rotate; Postgres FOR UPDATE, SQLite write txn. */ - getProviderForUpdate( - input: GetWebSearchProviderForUpdateInput, - transaction: TTransaction, - ): Promise; - createProvider(input: CreateWebSearchProviderInput, transaction?: TTransaction): Promise; + getProvider(tenantId: string, transaction?: TTransaction): Promise; + /** Single-row write: creates the provider or replaces the whole manifest. */ upsertProvider(input: UpsertWebSearchProviderInput, transaction?: TTransaction): Promise; } diff --git a/packages/trueforge/src/routes/catalogRoutes.ts b/packages/trueforge/src/routes/catalogRoutes.ts index 0111ca1c3..d5cf883a0 100644 --- a/packages/trueforge/src/routes/catalogRoutes.ts +++ b/packages/trueforge/src/routes/catalogRoutes.ts @@ -98,15 +98,14 @@ export const listWebSearchProviderCatalogRoute = createRoute({ method: 'get', path: '/web-search-providers', tags: [OpenApiTag.WEB_SEARCH], - summary: 'Get the web-search provider catalog', - description: - 'Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.', + summary: 'List web search providers', + description: 'Shipped web-search provider presets.', 'x-fern-sdk-group-name': ['catalogs', 'webSearchProviders'], 'x-fern-sdk-method-name': 'list', responses: { 200: { content: { 'application/json': { schema: GetWebSearchProviderCatalogResponseSchema } }, - description: 'Shipped web-search-provider presets.', + description: 'Shipped web-search provider presets.', }, 401: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, diff --git a/packages/trueforge/src/routes/openapiTags.ts b/packages/trueforge/src/routes/openapiTags.ts index a19c089e0..8f7fd5548 100644 --- a/packages/trueforge/src/routes/openapiTags.ts +++ b/packages/trueforge/src/routes/openapiTags.ts @@ -7,7 +7,7 @@ export enum OpenApiTag { MCP_SERVERS = 'MCP Servers', SKILLS = 'Skills', SANDBOXES = 'Sandboxes', - WEB_SEARCH = 'Web Search', + WEB_SEARCH = 'Web Search Providers', AGENTS = 'Agents', SCHEDULES = 'Schedules', AGENT_SESSIONS = 'Agent Sessions', diff --git a/packages/trueforge/src/routes/webSearchProviderRoutes.ts b/packages/trueforge/src/routes/webSearchProviderRoutes.ts index 32a3558d7..88392ef12 100644 --- a/packages/trueforge/src/routes/webSearchProviderRoutes.ts +++ b/packages/trueforge/src/routes/webSearchProviderRoutes.ts @@ -1,69 +1,32 @@ import { createRoute } from '@hono/zod-openapi'; import { RequestErrorResponseSchema } from '../schemas/errors'; -import { - CreateWebSearchProviderRequestSchema, - GetWebSearchProviderResponseSchema, - ListWebSearchProvidersResponseSchema, - UpdateWebSearchProviderRequestSchema, -} from '../schemas/webSearchProvider'; +import { GetWebSearchProviderResponseSchema, UpdateWebSearchProviderRequestSchema } from '../schemas/webSearchProvider'; import { OpenApiTag } from './openapiTags'; -export const listWebSearchProvidersRoute = createRoute({ +export const getWebSearchProviderRoute = createRoute({ method: 'get', path: '/', tags: [OpenApiTag.WEB_SEARCH], - summary: 'List configured web-search providers', - description: 'All configured providers with nested manifests. `auth.api_key` is redacted.', + summary: 'Get the web search provider', + description: 'The configured provider for this tenant. `auth.api_key` is redacted when present.', 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], - 'x-fern-sdk-method-name': 'list', + 'x-fern-sdk-method-name': 'get', responses: { 200: { - content: { 'application/json': { schema: ListWebSearchProvidersResponseSchema } }, - description: 'All configured web-search providers', + content: { 'application/json': { schema: GetWebSearchProviderResponseSchema } }, + description: 'The configured web search provider.', }, 401: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, - description: 'OIDC is configured and the request has no valid session cookie.', + description: 'Invalid Bearer token or auth cookie.', }, 403: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, - description: 'OIDC is configured and the caller is authenticated but not an admin.', - }, - }, -}); - -export const createWebSearchProviderRoute = createRoute({ - method: 'post', - path: '/', - tags: [OpenApiTag.WEB_SEARCH], - summary: 'Create a web-search provider', - description: - 'Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). ' + - '`auth.api_key`: real value required; redacted with no stored secret returns 400.', - 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], - 'x-fern-sdk-method-name': 'create', - request: { - body: { - content: { 'application/json': { schema: CreateWebSearchProviderRequestSchema } }, - required: true, - }, - }, - responses: { - 201: { - content: { 'application/json': { schema: GetWebSearchProviderResponseSchema } }, - description: 'The created provider', - }, - 400: { - content: { 'application/json': { schema: RequestErrorResponseSchema } }, - description: 'Invalid request body, or redacted API key with no stored secret to keep.', - }, - 409: { - content: { 'application/json': { schema: RequestErrorResponseSchema } }, - description: 'A web-search provider with this name already exists.', + description: 'Authenticated but not an admin.', }, - 424: { + 404: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, - description: 'Unsupported operation because web-search providers are managed by an external system', + description: 'No web search provider configured.', }, }, }); @@ -72,9 +35,9 @@ export const putWebSearchProviderRoute = createRoute({ method: 'put', path: '/', tags: [OpenApiTag.WEB_SEARCH], - summary: 'Create or replace a web-search provider', + summary: 'Create or replace the web search provider', description: - 'Create or replace a provider. Well-known types use `type` as `name` (one each). ' + + 'Upserts the single web search provider for this tenant. ' + '`auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).', 'x-fern-sdk-group-name': ['settings', 'webSearchProviders'], 'x-fern-sdk-method-name': 'create_or_update', @@ -87,7 +50,7 @@ export const putWebSearchProviderRoute = createRoute({ responses: { 200: { content: { 'application/json': { schema: GetWebSearchProviderResponseSchema } }, - description: 'The saved provider', + description: 'The saved provider.', }, 400: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, diff --git a/packages/trueforge/src/schemas/webSearchCatalog.ts b/packages/trueforge/src/schemas/webSearchCatalog.ts index 14f518c04..2b5e9b9f8 100644 --- a/packages/trueforge/src/schemas/webSearchCatalog.ts +++ b/packages/trueforge/src/schemas/webSearchCatalog.ts @@ -1,7 +1,6 @@ import { z } from '@hono/zod-openapi'; import { ParallelWebSearchProviderSchema } from './webSearchProvider'; -/** Single variant today (avoids one-member `oneOf` in OpenAPI). */ export const CatalogWebSearchProviderSchema = ParallelWebSearchProviderSchema.omit({ auth: true }) .strict() .openapi('CatalogWebSearchProvider'); diff --git a/packages/trueforge/src/schemas/webSearchProvider.ts b/packages/trueforge/src/schemas/webSearchProvider.ts index 3e3024d05..0bae26101 100644 --- a/packages/trueforge/src/schemas/webSearchProvider.ts +++ b/packages/trueforge/src/schemas/webSearchProvider.ts @@ -1,11 +1,6 @@ import { z } from '@hono/zod-openapi'; import { NameSchema, type ResourceName } from './common'; -export const ParallelWebSearchModeSchema = z - .enum(['turbo', 'fast', 'basic', 'advanced']) - .describe('Parallel Search mode preset.') - .openapi('ParallelWebSearchMode'); - const ParallelWebSearchProviderAuthSchema = z .object({ api_key: z @@ -19,16 +14,16 @@ const ParallelWebSearchProviderAuthSchema = z .describe('Parallel authentication credentials.') .openapi('ParallelWebSearchProviderAuth'); -/** Single variant today (avoids one-member `oneOf` in OpenAPI). */ export const ParallelWebSearchProviderSchema = z .object({ type: z.literal('parallel').describe('Parallel web-search provider.'), - auth: ParallelWebSearchProviderAuthSchema, - mode: ParallelWebSearchModeSchema, + auth: ParallelWebSearchProviderAuthSchema.optional(), }) .strict(); -export const WebSearchProviderManifestSchema = ParallelWebSearchProviderSchema.openapi('WebSearchProviderManifest'); +export const WebSearchProviderManifestSchema = z + .discriminatedUnion('type', [ParallelWebSearchProviderSchema]) + .openapi('WebSearchProviderManifest'); export function webSearchProviderName(provider: WebSearchProviderManifest): ResourceName { return provider.type; @@ -42,13 +37,6 @@ export const ConfiguredWebSearchProviderSchema = z .strict() .openapi('ConfiguredWebSearchProvider'); -export const CreateWebSearchProviderRequestSchema = z - .object({ - manifest: WebSearchProviderManifestSchema, - }) - .strict() - .openapi('CreateWebSearchProviderRequest'); - export const UpdateWebSearchProviderRequestSchema = z .object({ manifest: WebSearchProviderManifestSchema, @@ -62,14 +50,6 @@ export const GetWebSearchProviderResponseSchema = z }) .openapi('GetWebSearchProviderResponse'); -export const ListWebSearchProvidersResponseSchema = z - .object({ - data: z.array(ConfiguredWebSearchProviderSchema), - }) - .openapi('ListWebSearchProvidersResponse'); - -export type ParallelWebSearchMode = z.infer; export type WebSearchProviderManifest = z.infer; export type ConfiguredWebSearchProvider = z.infer; -export type CreateWebSearchProviderRequest = z.infer; export type UpdateWebSearchProviderRequest = z.infer; diff --git a/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts b/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts index 9b02ebe0e..d4007ea65 100644 --- a/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts +++ b/packages/trueforge/src/truefoundry/TrueFoundryWebSearchProviderStore.ts @@ -1,14 +1,9 @@ import configuration, { isTrueFoundryModeEnabled } from '../config'; import type { - CreateWebSearchProviderInput, - GetWebSearchProviderForUpdateInput, - GetWebSearchProviderInput, IWebSearchProviderStore, - ListWebSearchProvidersInput, UpsertWebSearchProviderInput, WebSearchProviderRecord, } from '../db/webSearchProviderStore'; -import { webSearchProviderName } from '../schemas/webSearchProvider'; import { trueFoundryManaged } from './errors'; function synthesizeParallelRecord(tenantId: string): WebSearchProviderRecord | undefined { @@ -19,53 +14,22 @@ function synthesizeParallelRecord(tenantId: string): WebSearchProviderRecord | u if (!env) { return undefined; } - const manifest = { - type: 'parallel' as const, - auth: { api_key: env.api_key }, - mode: 'turbo' as const, - }; const now = new Date().toISOString(); return { tenant_id: tenantId, - name: webSearchProviderName(manifest), - manifest, + manifest: { + type: 'parallel', + auth: { api_key: env.api_key }, + }, created_at: now, updated_at: now, }; } export class TrueFoundryWebSearchProviderStore implements IWebSearchProviderStore { - listProviders(input: ListWebSearchProvidersInput, transaction?: TTransaction): Promise { - void transaction; - const record = synthesizeParallelRecord(input.tenant_id); - return Promise.resolve(record ? [record] : []); - } - - getProvider( - input: GetWebSearchProviderInput, - transaction?: TTransaction, - ): Promise { - void transaction; - const record = synthesizeParallelRecord(input.tenant_id); - if (record?.name !== input.name) { - return Promise.resolve(undefined); - } - return Promise.resolve(record); - } - - getProviderForUpdate( - input: GetWebSearchProviderForUpdateInput, - transaction: TTransaction, - ): Promise { - void input; + getProvider(tenantId: string, transaction?: TTransaction): Promise { void transaction; - return trueFoundryManaged(); - } - - createProvider(input: CreateWebSearchProviderInput, transaction?: TTransaction): Promise { - void input; - void transaction; - return trueFoundryManaged(); + return Promise.resolve(synthesizeParallelRecord(tenantId)); } upsertProvider(input: UpsertWebSearchProviderInput, transaction?: TTransaction): Promise { diff --git a/packages/trueforge/src/websearch/providers.ts b/packages/trueforge/src/websearch/providers.ts index 8090d685e..3d63ca478 100644 --- a/packages/trueforge/src/websearch/providers.ts +++ b/packages/trueforge/src/websearch/providers.ts @@ -8,8 +8,8 @@ export async function hasConfiguredWebSearchProvider({ tenant_id: string; store: IWebSearchProviderStore; }): Promise { - const providers = await store.listProviders({ tenant_id }); - return providers.length > 0; + const record = await store.getProvider(tenant_id); + return record !== undefined; } export async function resolveWebSearchProvider({ @@ -19,11 +19,11 @@ export async function resolveWebSearchProvider({ tenant_id: string; store: IWebSearchProviderStore; }): Promise { - const providers = await store.listProviders({ tenant_id }); - const record = providers[0]; + const record = await store.getProvider(tenant_id); if (!record) { return undefined; } const { manifest } = record; - return new ParallelWebSearchProvider({ apiKey: manifest.auth.api_key, mode: manifest.mode }); + const apiKey = manifest.auth?.api_key; + return new ParallelWebSearchProvider(apiKey ? { apiKey, mode: 'turbo' } : { mode: 'turbo' }); } diff --git a/packages/trueforge/tests/unit/apis/capabilities.test.ts b/packages/trueforge/tests/unit/apis/capabilities.test.ts index 763a97464..e7efc58a7 100644 --- a/packages/trueforge/tests/unit/apis/capabilities.test.ts +++ b/packages/trueforge/tests/unit/apis/capabilities.test.ts @@ -70,19 +70,22 @@ describe('capabilities routers', () => { setCachedLocalSandboxSupport(undefined); }); - function makeRouter(authenticator: Authenticator = new StandaloneAuthenticator()): OpenAPIHono { + function makeRouter({ + authenticator = new StandaloneAuthenticator(), + webSearchStore, + }: { + authenticator?: Authenticator; + webSearchStore?: IWebSearchProviderStore; + } = {}): OpenAPIHono { const db = createSqliteDb(':memory:'); const emptyWebSearchStore: IWebSearchProviderStore = { - listProviders: () => Promise.resolve([]), getProvider: () => Promise.resolve(undefined), - getProviderForUpdate: () => Promise.resolve(undefined), - createProvider: () => Promise.reject(new Error('not used')), upsertProvider: () => Promise.reject(new Error('not used')), }; return withAuth( createCapabilitiesRouter({ resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), - resolveWebSearchProviderStore: () => emptyWebSearchStore, + resolveWebSearchProviderStore: () => webSearchStore ?? emptyWebSearchStore, withTransaction: callback => db.transaction().execute(callback), logger: silentLogger, resolveRequestContext, @@ -111,6 +114,28 @@ describe('capabilities routers', () => { }); }); + it('reports web_search enabled when a provider is configured', async () => { + disableOidcAuth(); + mockStatus.mockResolvedValue(undefined); + const configuredStore: IWebSearchProviderStore = { + getProvider: () => + Promise.resolve({ + tenant_id: 'default', + manifest: { type: 'parallel' }, + created_at: '2026-01-01T00:00:00.000Z', + updated_at: '2026-01-01T00:00:00.000Z', + }), + upsertProvider: () => Promise.reject(new Error('not used')), + }; + const router = makeRouter({ webSearchStore: configuredStore }); + + const response = await router.request('/'); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + data: { web_search: { enabled: true } }, + }); + }); + it('reports sandbox + skill enabled when local fallback is cached and no image status exists', async () => { disableOidcAuth(); mockStatus.mockResolvedValue(undefined); @@ -257,10 +282,7 @@ describe('capabilities routers', () => { const db = createSqliteDb(':memory:'); await migrateSqliteToLatest(db); const emptyWebSearchStore: IWebSearchProviderStore = { - listProviders: () => Promise.resolve([]), getProvider: () => Promise.resolve(undefined), - getProviderForUpdate: () => Promise.resolve(undefined), - createProvider: () => Promise.reject(new Error('not used')), upsertProvider: () => Promise.reject(new Error('not used')), }; const router = withAuth( diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index eb2e32da0..b8bdc9be2 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -20,8 +20,8 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; -import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import type { Database } from '../../../src/db/sqlite/types'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; diff --git a/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts b/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts new file mode 100644 index 000000000..8323e9da2 --- /dev/null +++ b/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts @@ -0,0 +1,85 @@ +import { createWebSearchProvidersRouter } from '../../../src/apis/webSearchProviders'; +import { STANDALONE_REQUEST_CONTEXT } from '../../../src/auth/identity'; +import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; +import { createSqliteDb } from '../../../src/db/sqlite/client'; +import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; +import { toRedactedSecretValue } from '../../../src/utils/secretRedaction'; + +function putInit(manifest: unknown): RequestInit { + return { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ manifest }), + }; +} + +async function createRouter() { + const db = createSqliteDb(':memory:'); + await migrateSqliteToLatest(db); + const store = new SqliteWebSearchProviderStore(db); + const router = createWebSearchProvidersRouter({ + resolveWebSearchProviderStore: () => store, + resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, + }); + return { router, store }; +} + +describe('settings web-search-providers router', () => { + it('GET returns 404 when none configured', async () => { + const { router } = await createRouter(); + const response = await router.request('/'); + expect(response.status).toBe(404); + expect(await response.json()).toEqual({ error: { message: 'No web search provider configured' } }); + }); + + it('PUT without api_key upserts the singleton and GET returns it', async () => { + const { router } = await createRouter(); + const put = await router.request('/', putInit({ type: 'parallel' })); + expect(put.status).toBe(200); + expect(await put.json()).toEqual({ + data: { name: 'parallel', manifest: { type: 'parallel' } }, + }); + + const get = await router.request('/'); + expect(get.status).toBe(200); + expect(await get.json()).toEqual({ + data: { name: 'parallel', manifest: { type: 'parallel' } }, + }); + }); + + it('PUT with api_key redacts on response and second PUT replaces the same row', async () => { + const { router, store } = await createRouter(); + const apiKey = 'parallel-secret-key'; + const created = await router.request('/', putInit({ type: 'parallel', auth: { api_key: apiKey } })); + expect(created.status).toBe(200); + expect(await created.json()).toEqual({ + data: { + name: 'parallel', + manifest: { type: 'parallel', auth: { api_key: toRedactedSecretValue(apiKey) } }, + }, + }); + + const first = await store.getProvider('default'); + expect(first?.manifest.auth?.api_key).toBe(apiKey); + + const rotated = await router.request('/', putInit({ type: 'parallel', auth: { api_key: 'parallel-rotated-key' } })); + expect(rotated.status).toBe(200); + + const second = await store.getProvider('default'); + expect(second?.manifest.auth?.api_key).toBe('parallel-rotated-key'); + expect(second?.created_at).toBe(first?.created_at); + }); + + it('PUT with redacted api_key keeps the stored secret', async () => { + const { router, store } = await createRouter(); + const apiKey = 'parallel-keep-secret'; + await router.request('/', putInit({ type: 'parallel', auth: { api_key: apiKey } })); + + const keep = await router.request( + '/', + putInit({ type: 'parallel', auth: { api_key: toRedactedSecretValue(apiKey) } }), + ); + expect(keep.status).toBe(200); + expect((await store.getProvider('default'))?.manifest.auth?.api_key).toBe(apiKey); + }); +}); From fb2085ea9cd935e1f60e93f91d053aca0fbf3b71 Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Tue, 22 Sep 2026 10:36:01 +0000 Subject: [PATCH 07/10] Regenerate OpenAPI document and SDKs --- .github/fern/openapi/openapi.json | 173 ++++------------ docs/openapi.json | 173 ++++------------ packages/trueforge-sdk/reference.md | 85 +------- .../webSearchProviders/client/Client.ts | 2 +- .../webSearchProviders/client/Client.ts | 149 ++------------ .../CreateWebSearchProviderRequest.ts | 19 -- .../UpdateWebSearchProviderRequest.ts | 4 - .../client/requests/index.ts | 1 - .../src/api/types/CatalogWebSearchProvider.ts | 3 - .../types/ListWebSearchProvidersResponse.ts | 7 - .../src/api/types/ParallelWebSearchMode.ts | 10 - .../api/types/WebSearchProviderManifest.ts | 3 +- packages/trueforge-sdk/src/api/types/index.ts | 2 - .../CreateWebSearchProviderRequest.ts | 19 -- .../client/requests/index.ts | 1 - .../types/CatalogWebSearchProvider.ts | 3 - .../types/ListWebSearchProvidersResponse.ts | 19 -- .../types/ParallelWebSearchMode.ts | 14 -- .../types/WebSearchProviderManifest.ts | 7 +- .../src/serialization/types/index.ts | 2 - .../wire/catalogs/webSearchProviders.test.ts | 3 +- .../wire/settings/webSearchProviders.test.ts | 167 +++------------ python/trueforge_sdk/reference.md | 94 +-------- .../src/trueforge_sdk/__init__.py | 6 - .../catalogs/web_search_providers/client.py | 8 +- .../web_search_providers/raw_client.py | 8 +- .../settings/web_search_providers/client.py | 155 ++------------ .../web_search_providers/raw_client.py | 193 +++--------------- .../src/trueforge_sdk/types/__init__.py | 6 - .../types/catalog_web_search_provider.py | 2 - .../list_web_search_providers_response.py | 20 -- .../types/parallel_web_search_mode.py | 46 ----- .../types/web_search_provider_manifest.py | 4 +- 33 files changed, 182 insertions(+), 1226 deletions(-) delete mode 100644 packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts delete mode 100644 packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts delete mode 100644 packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts delete mode 100644 packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts delete mode 100644 packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts delete mode 100644 packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts delete mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py delete mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 50bf3dc56..7e66cc868 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -715,9 +715,6 @@ "CatalogWebSearchProvider": { "additionalProperties": false, "properties": { - "mode": { - "$ref": "#/components/schemas/ParallelWebSearchMode" - }, "type": { "description": "Parallel web-search provider.", "enum": [ @@ -727,8 +724,7 @@ } }, "required": [ - "type", - "mode" + "type" ], "type": "object" }, @@ -1187,18 +1183,6 @@ }, "type": "object" }, - "CreateWebSearchProviderRequest": { - "additionalProperties": false, - "properties": { - "manifest": { - "$ref": "#/components/schemas/WebSearchProviderManifest" - } - }, - "required": [ - "manifest" - ], - "type": "object" - }, "CreatedBySubject": { "additionalProperties": false, "description": "Who created this resource.", @@ -2543,20 +2527,6 @@ ], "type": "object" }, - "ListWebSearchProvidersResponse": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/ConfiguredWebSearchProvider" - }, - "type": "array" - } - }, - "required": [ - "data" - ], - "type": "object" - }, "MCPAuthRequiredEvent": { "allOf": [ { @@ -3400,16 +3370,6 @@ ], "type": "object" }, - "ParallelWebSearchMode": { - "description": "Parallel Search mode preset.", - "enum": [ - "turbo", - "fast", - "basic", - "advanced" - ], - "type": "string" - }, "ParallelWebSearchProviderAuth": { "additionalProperties": false, "description": "Parallel authentication credentials.", @@ -5894,28 +5854,27 @@ "type": "object" }, "WebSearchProviderManifest": { - "additionalProperties": false, - "properties": { - "auth": { - "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" - }, - "mode": { - "$ref": "#/components/schemas/ParallelWebSearchMode" - }, - "type": { - "description": "Parallel web-search provider.", - "enum": [ - "parallel" + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "auth": { + "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type" ], - "type": "string" + "type": "object" } - }, - "required": [ - "type", - "auth", - "mode" - ], - "type": "object" + ] }, "ZaiModelProvider": { "additionalProperties": false, @@ -7229,7 +7188,7 @@ }, "/api/v1/catalogs/web-search-providers": { "get": { - "description": "Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.", + "description": "Shipped web-search provider presets.", "responses": { "200": { "content": { @@ -7239,7 +7198,7 @@ } } }, - "description": "Shipped web-search-provider presets." + "description": "Shipped web-search provider presets." }, "401": { "content": { @@ -7252,9 +7211,9 @@ "description": "Not authenticated." } }, - "summary": "Get the web-search provider catalog", + "summary": "List web search providers", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "catalogs", @@ -10152,17 +10111,17 @@ }, "/api/v1/settings/web-search-providers": { "get": { - "description": "All configured providers with nested manifests. `auth.api_key` is redacted.", + "description": "The configured provider for this tenant. `auth.api_key` is redacted when present.", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWebSearchProvidersResponse" + "$ref": "#/components/schemas/GetWebSearchProviderResponse" } } }, - "description": "All configured web-search providers" + "description": "The configured web search provider." }, "401": { "content": { @@ -10172,7 +10131,7 @@ } } }, - "description": "OIDC is configured and the request has no valid session cookie." + "description": "Invalid Bearer token or auth cookie." }, "403": { "content": { @@ -10182,63 +10141,9 @@ } } }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "List configured web-search providers", - "tags": [ - "Web Search" - ], - "x-fern-sdk-group-name": [ - "settings", - "webSearchProviders" - ], - "x-fern-sdk-method-name": "list" - }, - "post": { - "description": "Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWebSearchProviderRequest" - } - } + "description": "Authenticated but not an admin." }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetWebSearchProviderResponse" - } - } - }, - "description": "The created provider" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "Invalid request body, or redacted API key with no stored secret to keep." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "A web-search provider with this name already exists." - }, - "424": { + "404": { "content": { "application/json": { "schema": { @@ -10246,21 +10151,21 @@ } } }, - "description": "Unsupported operation because web-search providers are managed by an external system" + "description": "No web search provider configured." } }, - "summary": "Create a web-search provider", + "summary": "Get the web search provider", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "settings", "webSearchProviders" ], - "x-fern-sdk-method-name": "create" + "x-fern-sdk-method-name": "get" }, "put": { - "description": "Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", + "description": "Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", "requestBody": { "content": { "application/json": { @@ -10280,7 +10185,7 @@ } } }, - "description": "The saved provider" + "description": "The saved provider." }, "400": { "content": { @@ -10303,9 +10208,9 @@ "description": "Unsupported operation because web-search providers are managed by an external system" } }, - "summary": "Create or replace a web-search provider", + "summary": "Create or replace the web search provider", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "settings", @@ -10426,7 +10331,7 @@ "name": "Sandboxes" }, { - "name": "Web Search" + "name": "Web Search Providers" }, { "name": "Agents" diff --git a/docs/openapi.json b/docs/openapi.json index 50bf3dc56..7e66cc868 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -715,9 +715,6 @@ "CatalogWebSearchProvider": { "additionalProperties": false, "properties": { - "mode": { - "$ref": "#/components/schemas/ParallelWebSearchMode" - }, "type": { "description": "Parallel web-search provider.", "enum": [ @@ -727,8 +724,7 @@ } }, "required": [ - "type", - "mode" + "type" ], "type": "object" }, @@ -1187,18 +1183,6 @@ }, "type": "object" }, - "CreateWebSearchProviderRequest": { - "additionalProperties": false, - "properties": { - "manifest": { - "$ref": "#/components/schemas/WebSearchProviderManifest" - } - }, - "required": [ - "manifest" - ], - "type": "object" - }, "CreatedBySubject": { "additionalProperties": false, "description": "Who created this resource.", @@ -2543,20 +2527,6 @@ ], "type": "object" }, - "ListWebSearchProvidersResponse": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/ConfiguredWebSearchProvider" - }, - "type": "array" - } - }, - "required": [ - "data" - ], - "type": "object" - }, "MCPAuthRequiredEvent": { "allOf": [ { @@ -3400,16 +3370,6 @@ ], "type": "object" }, - "ParallelWebSearchMode": { - "description": "Parallel Search mode preset.", - "enum": [ - "turbo", - "fast", - "basic", - "advanced" - ], - "type": "string" - }, "ParallelWebSearchProviderAuth": { "additionalProperties": false, "description": "Parallel authentication credentials.", @@ -5894,28 +5854,27 @@ "type": "object" }, "WebSearchProviderManifest": { - "additionalProperties": false, - "properties": { - "auth": { - "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" - }, - "mode": { - "$ref": "#/components/schemas/ParallelWebSearchMode" - }, - "type": { - "description": "Parallel web-search provider.", - "enum": [ - "parallel" + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "auth": { + "$ref": "#/components/schemas/ParallelWebSearchProviderAuth" + }, + "type": { + "description": "Parallel web-search provider.", + "enum": [ + "parallel" + ], + "type": "string" + } + }, + "required": [ + "type" ], - "type": "string" + "type": "object" } - }, - "required": [ - "type", - "auth", - "mode" - ], - "type": "object" + ] }, "ZaiModelProvider": { "additionalProperties": false, @@ -7229,7 +7188,7 @@ }, "/api/v1/catalogs/web-search-providers": { "get": { - "description": "Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure.", + "description": "Shipped web-search provider presets.", "responses": { "200": { "content": { @@ -7239,7 +7198,7 @@ } } }, - "description": "Shipped web-search-provider presets." + "description": "Shipped web-search provider presets." }, "401": { "content": { @@ -7252,9 +7211,9 @@ "description": "Not authenticated." } }, - "summary": "Get the web-search provider catalog", + "summary": "List web search providers", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "catalogs", @@ -10152,17 +10111,17 @@ }, "/api/v1/settings/web-search-providers": { "get": { - "description": "All configured providers with nested manifests. `auth.api_key` is redacted.", + "description": "The configured provider for this tenant. `auth.api_key` is redacted when present.", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWebSearchProvidersResponse" + "$ref": "#/components/schemas/GetWebSearchProviderResponse" } } }, - "description": "All configured web-search providers" + "description": "The configured web search provider." }, "401": { "content": { @@ -10172,7 +10131,7 @@ } } }, - "description": "OIDC is configured and the request has no valid session cookie." + "description": "Invalid Bearer token or auth cookie." }, "403": { "content": { @@ -10182,63 +10141,9 @@ } } }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "List configured web-search providers", - "tags": [ - "Web Search" - ], - "x-fern-sdk-group-name": [ - "settings", - "webSearchProviders" - ], - "x-fern-sdk-method-name": "list" - }, - "post": { - "description": "Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWebSearchProviderRequest" - } - } + "description": "Authenticated but not an admin." }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetWebSearchProviderResponse" - } - } - }, - "description": "The created provider" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "Invalid request body, or redacted API key with no stored secret to keep." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "A web-search provider with this name already exists." - }, - "424": { + "404": { "content": { "application/json": { "schema": { @@ -10246,21 +10151,21 @@ } } }, - "description": "Unsupported operation because web-search providers are managed by an external system" + "description": "No web search provider configured." } }, - "summary": "Create a web-search provider", + "summary": "Get the web search provider", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "settings", "webSearchProviders" ], - "x-fern-sdk-method-name": "create" + "x-fern-sdk-method-name": "get" }, "put": { - "description": "Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", + "description": "Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", "requestBody": { "content": { "application/json": { @@ -10280,7 +10185,7 @@ } } }, - "description": "The saved provider" + "description": "The saved provider." }, "400": { "content": { @@ -10303,9 +10208,9 @@ "description": "Unsupported operation because web-search providers are managed by an external system" } }, - "summary": "Create or replace a web-search provider", + "summary": "Create or replace the web search provider", "tags": [ - "Web Search" + "Web Search Providers" ], "x-fern-sdk-group-name": [ "settings", @@ -10426,7 +10331,7 @@ "name": "Sandboxes" }, { - "name": "Web Search" + "name": "Web Search Providers" }, { "name": "Agents" diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 7d209dfad..80105d47f 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -2810,7 +2810,7 @@ await client.catalogs.skills.list();
-Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. +Shipped web-search provider presets.
@@ -4045,7 +4045,7 @@ await client.settings.skills.createOrUpdate({ ## Settings WebSearchProviders -
client.settings.webSearchProviders.list() -> TrueForge.ListWebSearchProvidersResponse +
client.settings.webSearchProviders.get() -> TrueForge.GetWebSearchProviderResponse
@@ -4057,7 +4057,7 @@ await client.settings.skills.createOrUpdate({
-All configured providers with nested manifests. `auth.api_key` is redacted. +The configured provider for this tenant. `auth.api_key` is redacted when present.
@@ -4072,7 +4072,7 @@ All configured providers with nested manifests. `auth.api_key` is redacted.
```typescript -await client.settings.webSearchProviders.list(); +await client.settings.webSearchProviders.get(); ```
@@ -4096,77 +4096,6 @@ await client.settings.webSearchProviders.list();
- - -
- -
client.settings.webSearchProviders.create({ ...params }) -> TrueForge.GetWebSearchProviderResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```typescript -await client.settings.webSearchProviders.create({ - manifest: { - auth: { - apiKey: "api_key" - }, - mode: "turbo", - type: "parallel" - } -}); - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `TrueForge.settings.CreateWebSearchProviderRequest` - -
-
- -
-
- -**requestOptions:** `WebSearchProvidersClient.RequestOptions` - -
-
-
-
- -
@@ -4183,7 +4112,7 @@ await client.settings.webSearchProviders.create({
-Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).
@@ -4200,10 +4129,6 @@ Create or replace a provider. Well-known types use `type` as `name` (one each). ```typescript await client.settings.webSearchProviders.createOrUpdate({ manifest: { - auth: { - apiKey: "api_key" - }, - mode: "turbo", type: "parallel" } }); diff --git a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts index 3e5f20f7e..90ba26dc8 100644 --- a/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/catalogs/resources/webSearchProviders/client/Client.ts @@ -23,7 +23,7 @@ export class WebSearchProvidersClient { } /** - * Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + * Shipped web-search provider presets. * * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. * diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts index 59e23ab4e..8caef57a5 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts @@ -24,27 +24,28 @@ export class WebSearchProvidersClient { } /** - * All configured providers with nested manifests. `auth.api_key` is redacted. + * The configured provider for this tenant. `auth.api_key` is redacted when present. * * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link TrueForge.UnauthorizedError} * @throws {@link TrueForge.ForbiddenError} + * @throws {@link TrueForge.NotFoundError} * @throws {@link errors.TrueForgeError} * @throws {@link errors.TrueForgeTimeoutError} * * @example - * await client.settings.webSearchProviders.list() + * await client.settings.webSearchProviders.get() */ - public list( + public get( requestOptions?: WebSearchProvidersClient.RequestOptions, - ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__list(requestOptions)); + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__get(requestOptions)); } - private async __list( + private async __get( requestOptions?: WebSearchProvidersClient.RequestOptions, - ): Promise> { + ): Promise> { const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( _authRequest.headers, @@ -68,7 +69,7 @@ export class WebSearchProvidersClient { }); if (_response.ok) { return { - data: serializers.ListWebSearchProvidersResponse.parseOrThrow(_response.body, { + data: serializers.GetWebSearchProviderResponse.parseOrThrow(_response.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, @@ -103,128 +104,8 @@ export class WebSearchProvidersClient { }), _response.rawResponse, ); - default: - throw new errors.TrueForgeError({ - statusCode: _response.error.statusCode, - body: _response.error.body, - rawResponse: _response.rawResponse, - }); - } - } - - return handleNonStatusCodeError( - _response.error, - _response.rawResponse, - "GET", - "/api/v1/settings/web-search-providers", - ); - } - - /** - * Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. - * - * @param {TrueForge.settings.CreateWebSearchProviderRequest} request - * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. - * - * @throws {@link TrueForge.BadRequestError} - * @throws {@link TrueForge.ConflictError} - * @throws {@link TrueForge.FailedDependencyError} - * @throws {@link errors.TrueForgeError} - * @throws {@link errors.TrueForgeTimeoutError} - * - * @example - * await client.settings.webSearchProviders.create({ - * manifest: { - * auth: { - * apiKey: "api_key" - * }, - * mode: "turbo", - * type: "parallel" - * } - * }) - */ - public create( - request: TrueForge.settings.CreateWebSearchProviderRequest, - requestOptions?: WebSearchProvidersClient.RequestOptions, - ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); - } - - private async __create( - request: TrueForge.settings.CreateWebSearchProviderRequest, - requestOptions?: WebSearchProvidersClient.RequestOptions, - ): Promise> { - const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); - const _headers: core.Fetcher.Args["headers"] = mergeHeaders( - _authRequest.headers, - this._options?.headers, - requestOptions?.headers, - ); - const _response = await (this._options.fetcher ?? core.fetcher)({ - url: core.url.join( - (await core.Supplier.get(this._options.baseUrl)) ?? - (await core.Supplier.get(this._options.environment)), - "api/v1/settings/web-search-providers", - ), - method: "POST", - headers: _headers, - contentType: "application/json", - queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), - requestType: "json", - body: mergeAdditionalBodyParameters( - serializers.settings.CreateWebSearchProviderRequest.jsonOrThrow(request, { - unrecognizedObjectKeys: "passthrough", - allowUnrecognizedUnionMembers: true, - allowUnrecognizedEnumValues: true, - omitUndefined: true, - }), - requestOptions?.additionalBodyParameters, - ), - timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, - maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, - abortSignal: requestOptions?.abortSignal, - fetchFn: this._options?.fetch, - logging: this._options.logging, - }); - if (_response.ok) { - return { - data: serializers.GetWebSearchProviderResponse.parseOrThrow(_response.body, { - unrecognizedObjectKeys: "passthrough", - allowUnrecognizedUnionMembers: true, - allowUnrecognizedEnumValues: true, - skipValidation: true, - breadcrumbsPrefix: ["response"], - }), - rawResponse: _response.rawResponse, - }; - } - - if (_response.error.reason === "status-code") { - switch (_response.error.statusCode) { - case 400: - throw new TrueForge.BadRequestError( - serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { - unrecognizedObjectKeys: "passthrough", - allowUnrecognizedUnionMembers: true, - allowUnrecognizedEnumValues: true, - skipValidation: true, - breadcrumbsPrefix: ["response"], - }), - _response.rawResponse, - ); - case 409: - throw new TrueForge.ConflictError( - serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { - unrecognizedObjectKeys: "passthrough", - allowUnrecognizedUnionMembers: true, - allowUnrecognizedEnumValues: true, - skipValidation: true, - breadcrumbsPrefix: ["response"], - }), - _response.rawResponse, - ); - case 424: - throw new TrueForge.FailedDependencyError( + case 404: + throw new TrueForge.NotFoundError( serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, @@ -246,13 +127,13 @@ export class WebSearchProvidersClient { return handleNonStatusCodeError( _response.error, _response.rawResponse, - "POST", + "GET", "/api/v1/settings/web-search-providers", ); } /** - * Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + * Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). * * @param {TrueForge.settings.UpdateWebSearchProviderRequest} request * @param {WebSearchProvidersClient.RequestOptions} requestOptions - Request-specific configuration. @@ -265,10 +146,6 @@ export class WebSearchProvidersClient { * @example * await client.settings.webSearchProviders.createOrUpdate({ * manifest: { - * auth: { - * apiKey: "api_key" - * }, - * mode: "turbo", * type: "parallel" * } * }) diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts deleted file mode 100644 index c8f06a67f..000000000 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../../../../../index.js"; - -/** - * @example - * { - * manifest: { - * auth: { - * apiKey: "api_key" - * }, - * mode: "turbo", - * type: "parallel" - * } - * } - */ -export interface CreateWebSearchProviderRequest { - manifest: TrueForge.WebSearchProviderManifest; -} diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts index 6da2c3a81..8223962b4 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts @@ -6,10 +6,6 @@ import type * as TrueForge from "../../../../../../index.js"; * @example * { * manifest: { - * auth: { - * apiKey: "api_key" - * }, - * mode: "turbo", * type: "parallel" * } * } diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts index d9efecc50..6b1f7c852 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/index.ts @@ -1,2 +1 @@ -export type { CreateWebSearchProviderRequest } from "./CreateWebSearchProviderRequest.js"; export type { UpdateWebSearchProviderRequest } from "./UpdateWebSearchProviderRequest.js"; diff --git a/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts b/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts index 54351e696..eb94bb2d6 100644 --- a/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts +++ b/packages/trueforge-sdk/src/api/types/CatalogWebSearchProvider.ts @@ -1,9 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as TrueForge from "../index.js"; - export interface CatalogWebSearchProvider { - mode: TrueForge.ParallelWebSearchMode; /** Parallel web-search provider. */ type: "parallel"; } diff --git a/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts b/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts deleted file mode 100644 index f2389877c..000000000 --- a/packages/trueforge-sdk/src/api/types/ListWebSearchProvidersResponse.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../index.js"; - -export interface ListWebSearchProvidersResponse { - data: TrueForge.ConfiguredWebSearchProvider[]; -} diff --git a/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts b/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts deleted file mode 100644 index b078158bd..000000000 --- a/packages/trueforge-sdk/src/api/types/ParallelWebSearchMode.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -/** Parallel Search mode preset. */ -export const ParallelWebSearchMode = { - Turbo: "turbo", - Fast: "fast", - Basic: "basic", - Advanced: "advanced", -} as const; -export type ParallelWebSearchMode = (typeof ParallelWebSearchMode)[keyof typeof ParallelWebSearchMode]; diff --git a/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts index c64bd7060..fbd77038e 100644 --- a/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts +++ b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts @@ -3,8 +3,7 @@ import type * as TrueForge from "../index.js"; export interface WebSearchProviderManifest { - auth: TrueForge.ParallelWebSearchProviderAuth; - mode: TrueForge.ParallelWebSearchMode; + auth?: TrueForge.ParallelWebSearchProviderAuth; /** Parallel web-search provider. */ type: "parallel"; } diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index e87768f70..3c8a2dcf3 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -106,7 +106,6 @@ export * from "./ListSkillVersionsResponse.js"; export * from "./ListTurnEventsOrder.js"; export * from "./ListTurnEventsResponse.js"; export * from "./ListTurnsResponse.js"; -export * from "./ListWebSearchProvidersResponse.js"; export * from "./McpAuthRequiredEvent.js"; export * from "./McpAuthStatus.js"; export * from "./McpAuthStatusStatus.js"; @@ -141,7 +140,6 @@ export * from "./ModelProviderAuth.js"; export * from "./ModelProviderManifest.js"; export * from "./MoonshotModelProvider.js"; export * from "./OpenAiModelProvider.js"; -export * from "./ParallelWebSearchMode.js"; export * from "./ParallelWebSearchProviderAuth.js"; export * from "./PermissionResourceType.js"; export * from "./PreviousTurnIdInput.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts deleted file mode 100644 index b074760a3..000000000 --- a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/CreateWebSearchProviderRequest.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../../../../../../api/index.js"; -import * as core from "../../../../../../../core/index.js"; -import type * as serializers from "../../../../../../index.js"; -import { WebSearchProviderManifest } from "../../../../../../types/WebSearchProviderManifest.js"; - -export const CreateWebSearchProviderRequest: core.serialization.Schema< - serializers.settings.CreateWebSearchProviderRequest.Raw, - TrueForge.settings.CreateWebSearchProviderRequest -> = core.serialization.object({ - manifest: WebSearchProviderManifest, -}); - -export declare namespace CreateWebSearchProviderRequest { - export interface Raw { - manifest: WebSearchProviderManifest.Raw; - } -} diff --git a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts index 5c31ff327..a8fe92d27 100644 --- a/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/settings/resources/webSearchProviders/client/requests/index.ts @@ -1,2 +1 @@ -export { CreateWebSearchProviderRequest } from "./CreateWebSearchProviderRequest.js"; export { UpdateWebSearchProviderRequest } from "./UpdateWebSearchProviderRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts b/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts index 6b3dc8ef4..1b6ec7bef 100644 --- a/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts +++ b/packages/trueforge-sdk/src/serialization/types/CatalogWebSearchProvider.ts @@ -3,19 +3,16 @@ import type * as TrueForge from "../../api/index.js"; import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; -import { ParallelWebSearchMode } from "./ParallelWebSearchMode.js"; export const CatalogWebSearchProvider: core.serialization.ObjectSchema< serializers.CatalogWebSearchProvider.Raw, TrueForge.CatalogWebSearchProvider > = core.serialization.object({ - mode: ParallelWebSearchMode, type: core.serialization.stringLiteral("parallel"), }); export declare namespace CatalogWebSearchProvider { export interface Raw { - mode: ParallelWebSearchMode.Raw; type: "parallel"; } } diff --git a/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts b/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts deleted file mode 100644 index 3e2142424..000000000 --- a/packages/trueforge-sdk/src/serialization/types/ListWebSearchProvidersResponse.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../api/index.js"; -import * as core from "../../core/index.js"; -import type * as serializers from "../index.js"; -import { ConfiguredWebSearchProvider } from "./ConfiguredWebSearchProvider.js"; - -export const ListWebSearchProvidersResponse: core.serialization.ObjectSchema< - serializers.ListWebSearchProvidersResponse.Raw, - TrueForge.ListWebSearchProvidersResponse -> = core.serialization.object({ - data: core.serialization.list(ConfiguredWebSearchProvider), -}); - -export declare namespace ListWebSearchProvidersResponse { - export interface Raw { - data: ConfiguredWebSearchProvider.Raw[]; - } -} diff --git a/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts b/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts deleted file mode 100644 index f9fd6a773..000000000 --- a/packages/trueforge-sdk/src/serialization/types/ParallelWebSearchMode.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../api/index.js"; -import * as core from "../../core/index.js"; -import type * as serializers from "../index.js"; - -export const ParallelWebSearchMode: core.serialization.Schema< - serializers.ParallelWebSearchMode.Raw, - TrueForge.ParallelWebSearchMode -> = core.serialization.enum_(["turbo", "fast", "basic", "advanced"]); - -export declare namespace ParallelWebSearchMode { - export type Raw = "turbo" | "fast" | "basic" | "advanced"; -} diff --git a/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts index d8bc89420..b92930362 100644 --- a/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts +++ b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts @@ -3,22 +3,19 @@ import type * as TrueForge from "../../api/index.js"; import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; -import { ParallelWebSearchMode } from "./ParallelWebSearchMode.js"; import { ParallelWebSearchProviderAuth } from "./ParallelWebSearchProviderAuth.js"; export const WebSearchProviderManifest: core.serialization.ObjectSchema< serializers.WebSearchProviderManifest.Raw, TrueForge.WebSearchProviderManifest > = core.serialization.object({ - auth: ParallelWebSearchProviderAuth, - mode: ParallelWebSearchMode, + auth: ParallelWebSearchProviderAuth.optional(), type: core.serialization.stringLiteral("parallel"), }); export declare namespace WebSearchProviderManifest { export interface Raw { - auth: ParallelWebSearchProviderAuth.Raw; - mode: ParallelWebSearchMode.Raw; + auth?: ParallelWebSearchProviderAuth.Raw | null; type: "parallel"; } } diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index e87768f70..3c8a2dcf3 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -106,7 +106,6 @@ export * from "./ListSkillVersionsResponse.js"; export * from "./ListTurnEventsOrder.js"; export * from "./ListTurnEventsResponse.js"; export * from "./ListTurnsResponse.js"; -export * from "./ListWebSearchProvidersResponse.js"; export * from "./McpAuthRequiredEvent.js"; export * from "./McpAuthStatus.js"; export * from "./McpAuthStatusStatus.js"; @@ -141,7 +140,6 @@ export * from "./ModelProviderAuth.js"; export * from "./ModelProviderManifest.js"; export * from "./MoonshotModelProvider.js"; export * from "./OpenAiModelProvider.js"; -export * from "./ParallelWebSearchMode.js"; export * from "./ParallelWebSearchProviderAuth.js"; export * from "./PermissionResourceType.js"; export * from "./PreviousTurnIdInput.js"; diff --git a/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts b/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts index 0db47cfe9..f07a981d4 100644 --- a/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts +++ b/packages/trueforge-sdk/tests/wire/catalogs/webSearchProviders.test.ts @@ -9,7 +9,7 @@ describe("WebSearchProvidersClient", () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawResponseBody = { data: [{ mode: "turbo", type: "parallel" }] }; + const rawResponseBody = { data: [{ type: "parallel" }] }; server .mockEndpoint() @@ -23,7 +23,6 @@ describe("WebSearchProvidersClient", () => { expect(response).toEqual({ data: [ { - mode: "turbo", type: "parallel", }, ], diff --git a/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts index 5b25cb3a1..5079d1883 100644 --- a/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts +++ b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts @@ -5,12 +5,12 @@ import { TrueForge } from "../../../src/Client"; import { mockServerPool } from "../../mock-server/MockServerPool"; describe("WebSearchProvidersClient", () => { - test("list (1)", async () => { + test("get (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawResponseBody = { - data: [{ manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }], + data: { manifest: { auth: { api_key: "api_key" }, type: "parallel" }, name: "name" }, }; server @@ -21,24 +21,21 @@ describe("WebSearchProvidersClient", () => { .jsonBody(rawResponseBody) .build(); - const response = await client.settings.webSearchProviders.list(); + const response = await client.settings.webSearchProviders.get(); expect(response).toEqual({ - data: [ - { - manifest: { - auth: { - apiKey: "api_key", - }, - mode: "turbo", - type: "parallel", + data: { + manifest: { + auth: { + apiKey: "api_key", }, - name: "name", + type: "parallel", }, - ], + name: "name", + }, }); }); - test("list (2)", async () => { + test("get (2)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); @@ -53,11 +50,11 @@ describe("WebSearchProvidersClient", () => { .build(); await expect(async () => { - return await client.settings.webSearchProviders.list(); + return await client.settings.webSearchProviders.get(); }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); }); - test("list (3)", async () => { + test("get (3)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); @@ -72,140 +69,35 @@ describe("WebSearchProvidersClient", () => { .build(); await expect(async () => { - return await client.settings.webSearchProviders.list(); + return await client.settings.webSearchProviders.get(); }).rejects.toThrow(TrueForgeTypes.ForbiddenError); }); - test("create (1)", async () => { - const server = mockServerPool.createServer(); - const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" } }; - const rawResponseBody = { - data: { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }, - }; - - server - .mockEndpoint() - .post("/api/v1/settings/web-search-providers") - .jsonBody(rawRequestBody) - .respondWith() - .statusCode(200) - .jsonBody(rawResponseBody) - .build(); - - const response = await client.settings.webSearchProviders.create({ - manifest: { - auth: { - apiKey: "api_key", - }, - mode: "turbo", - type: "parallel", - }, - }); - expect(response).toEqual({ - data: { - manifest: { - auth: { - apiKey: "api_key", - }, - mode: "turbo", - type: "parallel", - }, - name: "name", - }, - }); - }); - - test("create (2)", async () => { - const server = mockServerPool.createServer(); - const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; - const rawResponseBody = { error: { message: "message" } }; - - server - .mockEndpoint() - .post("/api/v1/settings/web-search-providers") - .jsonBody(rawRequestBody) - .respondWith() - .statusCode(400) - .jsonBody(rawResponseBody) - .build(); - - await expect(async () => { - return await client.settings.webSearchProviders.create({ - manifest: { - auth: { - apiKey: "x", - }, - mode: "turbo", - type: "parallel", - }, - }); - }).rejects.toThrow(TrueForgeTypes.BadRequestError); - }); - - test("create (3)", async () => { + test("get (4)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; - const rawResponseBody = { error: { message: "message" } }; - - server - .mockEndpoint() - .post("/api/v1/settings/web-search-providers") - .jsonBody(rawRequestBody) - .respondWith() - .statusCode(409) - .jsonBody(rawResponseBody) - .build(); - await expect(async () => { - return await client.settings.webSearchProviders.create({ - manifest: { - auth: { - apiKey: "x", - }, - mode: "turbo", - type: "parallel", - }, - }); - }).rejects.toThrow(TrueForgeTypes.ConflictError); - }); - - test("create (4)", async () => { - const server = mockServerPool.createServer(); - const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; const rawResponseBody = { error: { message: "message" } }; server .mockEndpoint() - .post("/api/v1/settings/web-search-providers") - .jsonBody(rawRequestBody) + .get("/api/v1/settings/web-search-providers") .respondWith() - .statusCode(424) + .statusCode(404) .jsonBody(rawResponseBody) .build(); await expect(async () => { - return await client.settings.webSearchProviders.create({ - manifest: { - auth: { - apiKey: "x", - }, - mode: "turbo", - type: "parallel", - }, - }); - }).rejects.toThrow(TrueForgeTypes.FailedDependencyError); + return await client.settings.webSearchProviders.get(); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); }); test("create_or_update (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" } }; + const rawRequestBody = { manifest: { type: "parallel" } }; const rawResponseBody = { - data: { manifest: { auth: { api_key: "api_key" }, mode: "turbo", type: "parallel" }, name: "name" }, + data: { manifest: { auth: { api_key: "api_key" }, type: "parallel" }, name: "name" }, }; server @@ -219,10 +111,6 @@ describe("WebSearchProvidersClient", () => { const response = await client.settings.webSearchProviders.createOrUpdate({ manifest: { - auth: { - apiKey: "api_key", - }, - mode: "turbo", type: "parallel", }, }); @@ -232,7 +120,6 @@ describe("WebSearchProvidersClient", () => { auth: { apiKey: "api_key", }, - mode: "turbo", type: "parallel", }, name: "name", @@ -243,7 +130,7 @@ describe("WebSearchProvidersClient", () => { test("create_or_update (2)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawRequestBody = { manifest: { type: "parallel" } }; const rawResponseBody = { error: { message: "message" } }; server @@ -258,10 +145,6 @@ describe("WebSearchProvidersClient", () => { await expect(async () => { return await client.settings.webSearchProviders.createOrUpdate({ manifest: { - auth: { - apiKey: "x", - }, - mode: "turbo", type: "parallel", }, }); @@ -271,7 +154,7 @@ describe("WebSearchProvidersClient", () => { test("create_or_update (3)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { auth: { api_key: "x" }, mode: "turbo", type: "parallel" } }; + const rawRequestBody = { manifest: { type: "parallel" } }; const rawResponseBody = { error: { message: "message" } }; server @@ -286,10 +169,6 @@ describe("WebSearchProvidersClient", () => { await expect(async () => { return await client.settings.webSearchProviders.createOrUpdate({ manifest: { - auth: { - apiKey: "x", - }, - mode: "turbo", type: "parallel", }, }); diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index 61643cd7e..da2d5d5df 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -3320,7 +3320,7 @@ client.catalogs.skills.list()
-Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. +Shipped web-search provider presets.
@@ -4754,7 +4754,7 @@ client.settings.skills.create_or_update(
## Settings WebSearchProviders -
client.settings.web_search_providers.list() -> ListWebSearchProvidersResponse +
client.settings.web_search_providers.get() -> GetWebSearchProviderResponse
@@ -4766,7 +4766,7 @@ client.settings.skills.create_or_update(
-All configured providers with nested manifests. `auth.api_key` is redacted. +The configured provider for this tenant. `auth.api_key` is redacted when present.
@@ -4788,7 +4788,7 @@ client = TrueForge( base_url="https://yourhost.com/path/to/api", ) -client.settings.web_search_providers.list() +client.settings.web_search_providers.get() ``` @@ -4812,84 +4812,6 @@ client.settings.web_search_providers.list()
- - -
- -
client.settings.web_search_providers.create(...) -> GetWebSearchProviderResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from trueforge_sdk import TrueForge, WebSearchProviderManifest, ParallelWebSearchProviderAuth, ParallelWebSearchMode - -client = TrueForge( - token="", - base_url="https://yourhost.com/path/to/api", -) - -client.settings.web_search_providers.create( - manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, - type="parallel", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**manifest:** `WebSearchProviderManifest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- -
@@ -4906,7 +4828,7 @@ client.settings.web_search_providers.create(
-Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). +Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).
@@ -4921,7 +4843,7 @@ Create or replace a provider. Well-known types use `type` as `name` (one each).
```python -from trueforge_sdk import TrueForge, WebSearchProviderManifest, ParallelWebSearchProviderAuth, ParallelWebSearchMode +from trueforge_sdk import TrueForge, WebSearchProviderManifest client = TrueForge( token="", @@ -4930,10 +4852,6 @@ client = TrueForge( client.settings.web_search_providers.create_or_update( manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, type="parallel", ), ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index ce622a1eb..9b49e63fa 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -115,7 +115,6 @@ ListTurnEventsOrder, ListTurnEventsResponse, ListTurnsResponse, - ListWebSearchProvidersResponse, McpAuthRequiredEvent, McpAuthStatus, McpAuthStatusStatus, @@ -150,7 +149,6 @@ ModelProviderManifest, MoonshotModelProvider, OpenAiModelProvider, - ParallelWebSearchMode, ParallelWebSearchProviderAuth, PermissionResourceType, PreviousTurnIdInput, @@ -391,7 +389,6 @@ "ListTurnEventsOrder": ".types", "ListTurnEventsResponse": ".types", "ListTurnsResponse": ".types", - "ListWebSearchProvidersResponse": ".types", "McpAuthRequiredEvent": ".types", "McpAuthStatus": ".types", "McpAuthStatusStatus": ".types", @@ -427,7 +424,6 @@ "MoonshotModelProvider": ".types", "NotFoundError": ".errors", "OpenAiModelProvider": ".types", - "ParallelWebSearchMode": ".types", "ParallelWebSearchProviderAuth": ".types", "PermissionResourceType": ".types", "PreconditionFailedError": ".errors", @@ -689,7 +685,6 @@ def __dir__(): "ListTurnEventsOrder", "ListTurnEventsResponse", "ListTurnsResponse", - "ListWebSearchProvidersResponse", "McpAuthRequiredEvent", "McpAuthStatus", "McpAuthStatusStatus", @@ -725,7 +720,6 @@ def __dir__(): "MoonshotModelProvider", "NotFoundError", "OpenAiModelProvider", - "ParallelWebSearchMode", "ParallelWebSearchProviderAuth", "PermissionResourceType", "PreconditionFailedError", diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py index 2ee84b4b7..4bb6f981c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/client.py @@ -25,7 +25,7 @@ def with_raw_response(self) -> RawWebSearchProvidersClient: def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetWebSearchProviderCatalogResponse: """ - Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + Shipped web-search provider presets. Parameters ---------- @@ -35,7 +35,7 @@ def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> Ge Returns ------- GetWebSearchProviderCatalogResponse - Shipped web-search-provider presets. + Shipped web-search provider presets. Examples -------- @@ -70,7 +70,7 @@ async def list( self, *, request_options: typing.Optional[RequestOptions] = None ) -> GetWebSearchProviderCatalogResponse: """ - Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + Shipped web-search provider presets. Parameters ---------- @@ -80,7 +80,7 @@ async def list( Returns ------- GetWebSearchProviderCatalogResponse - Shipped web-search-provider presets. + Shipped web-search provider presets. Examples -------- diff --git a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py index 3f1c19981..15a2ac908 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/catalogs/web_search_providers/raw_client.py @@ -23,7 +23,7 @@ def list( self, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[GetWebSearchProviderCatalogResponse]: """ - Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + Shipped web-search provider presets. Parameters ---------- @@ -33,7 +33,7 @@ def list( Returns ------- HttpResponse[GetWebSearchProviderCatalogResponse] - Shipped web-search-provider presets. + Shipped web-search provider presets. """ _response = self._client_wrapper.httpx_client.request( "api/v1/catalogs/web-search-providers", @@ -79,7 +79,7 @@ async def list( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[GetWebSearchProviderCatalogResponse]: """ - Shipped web-search-provider presets (discovery-only). Copy into POST/PUT /settings/web-search-providers to configure. + Shipped web-search provider presets. Parameters ---------- @@ -89,7 +89,7 @@ async def list( Returns ------- AsyncHttpResponse[GetWebSearchProviderCatalogResponse] - Shipped web-search-provider presets. + Shipped web-search provider presets. """ _response = await self._client_wrapper.httpx_client.request( "api/v1/catalogs/web-search-providers", diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py index 1ae9bcba8..a27a27e9f 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py @@ -5,7 +5,6 @@ from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ...core.request_options import RequestOptions from ...types.get_web_search_provider_response import GetWebSearchProviderResponse -from ...types.list_web_search_providers_response import ListWebSearchProvidersResponse from ...types.web_search_provider_manifest import WebSearchProviderManifest from .raw_client import AsyncRawWebSearchProvidersClient, RawWebSearchProvidersClient @@ -28,81 +27,38 @@ def with_raw_response(self) -> RawWebSearchProvidersClient: """ return self._raw_client - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListWebSearchProvidersResponse: + def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetWebSearchProviderResponse: """ - All configured providers with nested manifests. `auth.api_key` is redacted. + The configured provider for this tenant. `auth.api_key` is redacted when present. Parameters ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListWebSearchProvidersResponse - All configured web-search providers - - Examples - -------- - from trueforge_sdk import TrueForge - - client = TrueForge( - token="YOUR_TOKEN", - base_url="https://yourhost.com/path/to/api", - ) - client.settings.web_search_providers.list() - """ - _response = self._raw_client.list(request_options=request_options) - return _response.data - - def create( - self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None - ) -> GetWebSearchProviderResponse: - """ - Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. - - Parameters - ---------- - manifest : WebSearchProviderManifest - request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- GetWebSearchProviderResponse - The created provider + The configured web search provider. Examples -------- - from trueforge_sdk import ( - ParallelWebSearchMode, - ParallelWebSearchProviderAuth, - TrueForge, - WebSearchProviderManifest, - ) + from trueforge_sdk import TrueForge client = TrueForge( token="YOUR_TOKEN", base_url="https://yourhost.com/path/to/api", ) - client.settings.web_search_providers.create( - manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, - ), - ) + client.settings.web_search_providers.get() """ - _response = self._raw_client.create(manifest=manifest, request_options=request_options) + _response = self._raw_client.get(request_options=request_options) return _response.data def create_or_update( self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None ) -> GetWebSearchProviderResponse: """ - Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). Parameters ---------- @@ -114,28 +70,18 @@ def create_or_update( Returns ------- GetWebSearchProviderResponse - The saved provider + The saved provider. Examples -------- - from trueforge_sdk import ( - ParallelWebSearchMode, - ParallelWebSearchProviderAuth, - TrueForge, - WebSearchProviderManifest, - ) + from trueforge_sdk import TrueForge, WebSearchProviderManifest client = TrueForge( token="YOUR_TOKEN", base_url="https://yourhost.com/path/to/api", ) client.settings.web_search_providers.create_or_update( - manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, - ), + manifest=WebSearchProviderManifest(), ) """ _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) @@ -157,69 +103,25 @@ def with_raw_response(self) -> AsyncRawWebSearchProvidersClient: """ return self._raw_client - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListWebSearchProvidersResponse: - """ - All configured providers with nested manifests. `auth.api_key` is redacted. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListWebSearchProvidersResponse - All configured web-search providers - - Examples - -------- - import asyncio - - from trueforge_sdk import AsyncTrueForge - - client = AsyncTrueForge( - token="YOUR_TOKEN", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.settings.web_search_providers.list() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list(request_options=request_options) - return _response.data - - async def create( - self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None - ) -> GetWebSearchProviderResponse: + async def get(self, *, request_options: typing.Optional[RequestOptions] = None) -> GetWebSearchProviderResponse: """ - Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. + The configured provider for this tenant. `auth.api_key` is redacted when present. Parameters ---------- - manifest : WebSearchProviderManifest - request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- GetWebSearchProviderResponse - The created provider + The configured web search provider. Examples -------- import asyncio - from trueforge_sdk import ( - AsyncTrueForge, - ParallelWebSearchMode, - ParallelWebSearchProviderAuth, - WebSearchProviderManifest, - ) + from trueforge_sdk import AsyncTrueForge client = AsyncTrueForge( token="YOUR_TOKEN", @@ -228,26 +130,19 @@ async def create( async def main() -> None: - await client.settings.web_search_providers.create( - manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, - ), - ) + await client.settings.web_search_providers.get() asyncio.run(main()) """ - _response = await self._raw_client.create(manifest=manifest, request_options=request_options) + _response = await self._raw_client.get(request_options=request_options) return _response.data async def create_or_update( self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None ) -> GetWebSearchProviderResponse: """ - Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). Parameters ---------- @@ -259,18 +154,13 @@ async def create_or_update( Returns ------- GetWebSearchProviderResponse - The saved provider + The saved provider. Examples -------- import asyncio - from trueforge_sdk import ( - AsyncTrueForge, - ParallelWebSearchMode, - ParallelWebSearchProviderAuth, - WebSearchProviderManifest, - ) + from trueforge_sdk import AsyncTrueForge, WebSearchProviderManifest client = AsyncTrueForge( token="YOUR_TOKEN", @@ -280,12 +170,7 @@ async def create_or_update( async def main() -> None: await client.settings.web_search_providers.create_or_update( - manifest=WebSearchProviderManifest( - auth=ParallelWebSearchProviderAuth( - api_key="api_key", - ), - mode=ParallelWebSearchMode.TURBO, - ), + manifest=WebSearchProviderManifest(), ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py index c43c0f18a..339d97d29 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/raw_client.py @@ -11,12 +11,11 @@ from ...core.serialization import convert_and_respect_annotation_metadata from ...core.unchecked_base_model import construct_type from ...errors.bad_request_error import BadRequestError -from ...errors.conflict_error import ConflictError from ...errors.failed_dependency_error import FailedDependencyError from ...errors.forbidden_error import ForbiddenError +from ...errors.not_found_error import NotFoundError from ...errors.unauthorized_error import UnauthorizedError from ...types.get_web_search_provider_response import GetWebSearchProviderResponse -from ...types.list_web_search_providers_response import ListWebSearchProvidersResponse from ...types.request_error_response import RequestErrorResponse from ...types.web_search_provider_manifest import WebSearchProviderManifest from pydantic import ValidationError @@ -29,11 +28,11 @@ class RawWebSearchProvidersClient: def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper - def list( + def get( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ListWebSearchProvidersResponse]: + ) -> HttpResponse[GetWebSearchProviderResponse]: """ - All configured providers with nested manifests. `auth.api_key` is redacted. + The configured provider for this tenant. `auth.api_key` is redacted when present. Parameters ---------- @@ -42,8 +41,8 @@ def list( Returns ------- - HttpResponse[ListWebSearchProvidersResponse] - All configured web-search providers + HttpResponse[GetWebSearchProviderResponse] + The configured web search provider. """ _response = self._client_wrapper.httpx_client.request( "api/v1/settings/web-search-providers", @@ -53,9 +52,9 @@ def list( try: if 200 <= _response.status_code < 300: _data = typing.cast( - ListWebSearchProvidersResponse, + GetWebSearchProviderResponse, construct_type( - type_=ListWebSearchProvidersResponse, # type: ignore + type_=GetWebSearchProviderResponse, # type: ignore object_=_response.json(), ), ) @@ -82,81 +81,8 @@ def list( ), ), ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def create( - self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[GetWebSearchProviderResponse]: - """ - Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. - - Parameters - ---------- - manifest : WebSearchProviderManifest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GetWebSearchProviderResponse] - The created provider - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/settings/web-search-providers", - method="POST", - json={ - "manifest": convert_and_respect_annotation_metadata( - object_=manifest, annotation=WebSearchProviderManifest, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GetWebSearchProviderResponse, - construct_type( - type_=GetWebSearchProviderResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - RequestErrorResponse, - construct_type( - type_=RequestErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - RequestErrorResponse, - construct_type( - type_=RequestErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 424: - raise FailedDependencyError( + if _response.status_code == 404: + raise NotFoundError( headers=dict(_response.headers), body=typing.cast( RequestErrorResponse, @@ -179,7 +105,7 @@ def create_or_update( self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[GetWebSearchProviderResponse]: """ - Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). Parameters ---------- @@ -191,7 +117,7 @@ def create_or_update( Returns ------- HttpResponse[GetWebSearchProviderResponse] - The saved provider + The saved provider. """ _response = self._client_wrapper.httpx_client.request( "api/v1/settings/web-search-providers", @@ -253,11 +179,11 @@ class AsyncRawWebSearchProvidersClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper - async def list( + async def get( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ListWebSearchProvidersResponse]: + ) -> AsyncHttpResponse[GetWebSearchProviderResponse]: """ - All configured providers with nested manifests. `auth.api_key` is redacted. + The configured provider for this tenant. `auth.api_key` is redacted when present. Parameters ---------- @@ -266,8 +192,8 @@ async def list( Returns ------- - AsyncHttpResponse[ListWebSearchProvidersResponse] - All configured web-search providers + AsyncHttpResponse[GetWebSearchProviderResponse] + The configured web search provider. """ _response = await self._client_wrapper.httpx_client.request( "api/v1/settings/web-search-providers", @@ -277,9 +203,9 @@ async def list( try: if 200 <= _response.status_code < 300: _data = typing.cast( - ListWebSearchProvidersResponse, + GetWebSearchProviderResponse, construct_type( - type_=ListWebSearchProvidersResponse, # type: ignore + type_=GetWebSearchProviderResponse, # type: ignore object_=_response.json(), ), ) @@ -306,81 +232,8 @@ async def list( ), ), ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def create( - self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[GetWebSearchProviderResponse]: - """ - Creates a provider. Fails if `name` is already taken. Well-known types use `type` as `name` (one each). `auth.api_key`: real value required; redacted with no stored secret returns 400. - - Parameters - ---------- - manifest : WebSearchProviderManifest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GetWebSearchProviderResponse] - The created provider - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/settings/web-search-providers", - method="POST", - json={ - "manifest": convert_and_respect_annotation_metadata( - object_=manifest, annotation=WebSearchProviderManifest, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GetWebSearchProviderResponse, - construct_type( - type_=GetWebSearchProviderResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - RequestErrorResponse, - construct_type( - type_=RequestErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - RequestErrorResponse, - construct_type( - type_=RequestErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 424: - raise FailedDependencyError( + if _response.status_code == 404: + raise NotFoundError( headers=dict(_response.headers), body=typing.cast( RequestErrorResponse, @@ -403,7 +256,7 @@ async def create_or_update( self, *, manifest: WebSearchProviderManifest, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[GetWebSearchProviderResponse]: """ - Create or replace a provider. Well-known types use `type` as `name` (one each). `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). + Upserts the single web search provider for this tenant. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none). Parameters ---------- @@ -415,7 +268,7 @@ async def create_or_update( Returns ------- AsyncHttpResponse[GetWebSearchProviderResponse] - The saved provider + The saved provider. """ _response = await self._client_wrapper.httpx_client.request( "api/v1/settings/web-search-providers", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index 04094eaf4..07d9b1918 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -114,7 +114,6 @@ from .list_turn_events_order import ListTurnEventsOrder from .list_turn_events_response import ListTurnEventsResponse from .list_turns_response import ListTurnsResponse - from .list_web_search_providers_response import ListWebSearchProvidersResponse from .mcp_auth_required_event import McpAuthRequiredEvent from .mcp_auth_status import McpAuthStatus from .mcp_auth_status_status import McpAuthStatusStatus @@ -149,7 +148,6 @@ from .model_provider_manifest import ModelProviderManifest from .moonshot_model_provider import MoonshotModelProvider from .open_ai_model_provider import OpenAiModelProvider - from .parallel_web_search_mode import ParallelWebSearchMode from .parallel_web_search_provider_auth import ParallelWebSearchProviderAuth from .permission_resource_type import PermissionResourceType from .previous_turn_id_input import PreviousTurnIdInput @@ -360,7 +358,6 @@ "ListTurnEventsOrder": ".list_turn_events_order", "ListTurnEventsResponse": ".list_turn_events_response", "ListTurnsResponse": ".list_turns_response", - "ListWebSearchProvidersResponse": ".list_web_search_providers_response", "McpAuthRequiredEvent": ".mcp_auth_required_event", "McpAuthStatus": ".mcp_auth_status", "McpAuthStatusStatus": ".mcp_auth_status_status", @@ -395,7 +392,6 @@ "ModelProviderManifest": ".model_provider_manifest", "MoonshotModelProvider": ".moonshot_model_provider", "OpenAiModelProvider": ".open_ai_model_provider", - "ParallelWebSearchMode": ".parallel_web_search_mode", "ParallelWebSearchProviderAuth": ".parallel_web_search_provider_auth", "PermissionResourceType": ".permission_resource_type", "PreviousTurnIdInput": ".previous_turn_id_input", @@ -630,7 +626,6 @@ def __dir__(): "ListTurnEventsOrder", "ListTurnEventsResponse", "ListTurnsResponse", - "ListWebSearchProvidersResponse", "McpAuthRequiredEvent", "McpAuthStatus", "McpAuthStatusStatus", @@ -665,7 +660,6 @@ def __dir__(): "ModelProviderManifest", "MoonshotModelProvider", "OpenAiModelProvider", - "ParallelWebSearchMode", "ParallelWebSearchProviderAuth", "PermissionResourceType", "PreviousTurnIdInput", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py index 6e315caae..7ec58278c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/catalog_web_search_provider.py @@ -5,11 +5,9 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2 from ..core.unchecked_base_model import UncheckedBaseModel -from .parallel_web_search_mode import ParallelWebSearchMode class CatalogWebSearchProvider(UncheckedBaseModel): - mode: ParallelWebSearchMode type: typing.Literal["parallel"] = pydantic.Field(default="parallel") """ Parallel web-search provider. diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py deleted file mode 100644 index abd50453c..000000000 --- a/python/trueforge_sdk/src/trueforge_sdk/types/list_web_search_providers_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2 -from ..core.unchecked_base_model import UncheckedBaseModel -from .configured_web_search_provider import ConfiguredWebSearchProvider - - -class ListWebSearchProvidersResponse(UncheckedBaseModel): - data: typing.List[ConfiguredWebSearchProvider] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 - else: - - class Config: - smart_union = True - extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py b/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py deleted file mode 100644 index 632c3d7bf..000000000 --- a/python/trueforge_sdk/src/trueforge_sdk/types/parallel_web_search_mode.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core import enum - -T_Result = typing.TypeVar("T_Result") - - -class ParallelWebSearchMode(enum.StrEnum): - """ - Parallel Search mode preset. - """ - - TURBO = "turbo" - FAST = "fast" - BASIC = "basic" - ADVANCED = "advanced" - _UNKNOWN = "__PARALLELWEBSEARCHMODE_UNKNOWN__" - """ - This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. - """ - - @classmethod - def _missing_(cls, value: typing.Any) -> "ParallelWebSearchMode": - unknown = cls._UNKNOWN - unknown._value_ = value - return unknown - - def visit( - self, - turbo: typing.Callable[[], T_Result], - fast: typing.Callable[[], T_Result], - basic: typing.Callable[[], T_Result], - advanced: typing.Callable[[], T_Result], - _unknown_member: typing.Callable[[str], T_Result], - ) -> T_Result: - if self is ParallelWebSearchMode.TURBO: - return turbo() - if self is ParallelWebSearchMode.FAST: - return fast() - if self is ParallelWebSearchMode.BASIC: - return basic() - if self is ParallelWebSearchMode.ADVANCED: - return advanced() - return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py index 84d169259..67250259b 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py @@ -5,13 +5,11 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2 from ..core.unchecked_base_model import UncheckedBaseModel -from .parallel_web_search_mode import ParallelWebSearchMode from .parallel_web_search_provider_auth import ParallelWebSearchProviderAuth class WebSearchProviderManifest(UncheckedBaseModel): - auth: ParallelWebSearchProviderAuth - mode: ParallelWebSearchMode + auth: typing.Optional[ParallelWebSearchProviderAuth] = None type: typing.Literal["parallel"] = pydantic.Field(default="parallel") """ Parallel web-search provider. From 55fdc7760ea623a44367f8287abfc8ddea37f8bc Mon Sep 17 00:00:00 2001 From: thesujai Date: Tue, 22 Sep 2026 16:34:43 +0530 Subject: [PATCH 08/10] Add support for new web search provider settings and UI enhancements --- .changeset/oss-web-search-providers.md | 4 +- packages/assistant-ui-runtime/src/index.ts | 3 - .../assistant-ui-runtime/src/server/index.ts | 3 - .../assistant-ui-runtime/src/server/types.ts | 23 ++---- .../web-search/ParallelWebSearchProvider.ts | 5 +- .../ConfigureWebSearchForm.tsx | 56 +------------- .../SettingsBuilder/WebSearchSettings.tsx | 35 +-------- packages/trueforge-ui/src/index.ts | 2 - .../catalogs/webSearchProviderCatalog.ts | 77 +++++++------------ .../trueforge-agent-server-adapter/index.ts | 1 - packages/trueforge-ui/src/server/types.ts | 3 - .../WebSearchSettings.test.tsx | 30 +------- .../catalogs/webSearchProviderCatalog.test.ts | 25 +----- .../trueforge/src/apis/webSearchProviders.ts | 8 +- .../src/catalog/webSearchCatalog.gen.ts | 2 +- .../src/schemas/webSearchProvider.ts | 2 +- packages/trueforge/src/websearch/providers.ts | 3 +- .../tests/unit/apis/capabilities.test.ts | 2 +- .../unit/apis/webSearchProviders.test.ts | 30 ++++---- 19 files changed, 71 insertions(+), 243 deletions(-) diff --git a/.changeset/oss-web-search-providers.md b/.changeset/oss-web-search-providers.md index 518737b21..9875faf1d 100644 --- a/.changeset/oss-web-search-providers.md +++ b/.changeset/oss-web-search-providers.md @@ -1,6 +1,8 @@ --- "@truefoundry/trueforge": minor "@truefoundry/trueforge-core": minor +"@truefoundry/trueforge-ui": minor +"@truefoundry/trueforge-assistant-ui-runtime": minor --- -Add OSS web-search provider settings and catalog (Parallel): named `web_search_provider` rows, settings/catalog APIs, and tenant-aware host resolution so built-in web search works outside TrueFoundry mode. +Add OSS web-search provider settings and catalog (Parallel): singleton settings/catalog APIs, optional API key, and UI adapter without mode config so built-in web search works outside TrueFoundry mode. diff --git a/packages/assistant-ui-runtime/src/index.ts b/packages/assistant-ui-runtime/src/index.ts index d56cfdf8f..cfdd8e993 100644 --- a/packages/assistant-ui-runtime/src/index.ts +++ b/packages/assistant-ui-runtime/src/index.ts @@ -158,7 +158,6 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, - ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -223,8 +222,6 @@ export type { WebSearchBase, WebSearchCatalogEntry, WebSearchCatalogServer, - WebSearchConfig, WebSearchProviderBase, WebSearchProviderCatalogEntry, - WebSearchProviderConfig, } from './server/index.js'; diff --git a/packages/assistant-ui-runtime/src/server/index.ts b/packages/assistant-ui-runtime/src/server/index.ts index c8742e0ce..721400328 100644 --- a/packages/assistant-ui-runtime/src/server/index.ts +++ b/packages/assistant-ui-runtime/src/server/index.ts @@ -82,7 +82,6 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, - ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -142,10 +141,8 @@ export type { WebSearchBase, WebSearchCatalogEntry, WebSearchCatalogServer, - WebSearchConfig, WebSearchProviderBase, WebSearchProviderCatalogEntry, - WebSearchProviderConfig, } from './types.js'; export type { diff --git a/packages/assistant-ui-runtime/src/server/types.ts b/packages/assistant-ui-runtime/src/server/types.ts index 8e6cbac63..1a09b12a7 100644 --- a/packages/assistant-ui-runtime/src/server/types.ts +++ b/packages/assistant-ui-runtime/src/server/types.ts @@ -798,18 +798,7 @@ export interface SandboxCatalogServer< // Web-search providers catalog — public rows omit credentials; writes accept them // --------------------------------------------------------------------------- -/** - * Parallel Search mode preset (Parallel-specific, not a global web-search mode). - * A future provider would carry its own config fields instead of sharing this enum. - */ -export type ParallelWebSearchMode = 'turbo' | 'fast' | 'basic' | 'advanced'; - -/** Mutable web-search settings shared by catalog rows, create, and update. */ -export interface WebSearchConfig { - mode: ParallelWebSearchMode; -} - -export interface WebSearchCatalogEntry extends WebSearchConfig { +export interface WebSearchCatalogEntry { id: string; name: string; type: string; @@ -817,16 +806,15 @@ export interface WebSearchCatalogEntry extends WebSearchConfig { /** * Connected web-search provider row (settings/web-search). No raw `apiKey`. - * Includes last-saved config so update forms can show previous values. */ -export interface WebSearchBase extends WebSearchConfig { +export interface WebSearchBase { id: string; name: string; catalogId: string; isConnected: boolean; } -export interface CreateWebSearchRequest extends WebSearchConfig { +export interface CreateWebSearchRequest { /** `WebSearchCatalogEntry.id` used to create this web-search provider. */ catalogId: string; name: string; @@ -834,14 +822,13 @@ export interface CreateWebSearchRequest extends WebSearchConfig { apiKey: string; } -export interface UpdateWebSearchRequest extends WebSearchConfig { +export interface UpdateWebSearchRequest { id: string; - /** Omit to keep the existing key; send a value to rotate. */ + /** Omit to keep the existing key; send a value to set/rotate. */ apiKey?: string; } /** Host-facing aliases (trueforge-ui public names). */ -export type WebSearchProviderConfig = WebSearchConfig; export type WebSearchProviderCatalogEntry = WebSearchCatalogEntry; export type WebSearchProviderBase = WebSearchBase; export type CreateWebSearchProviderRequest = CreateWebSearchRequest; diff --git a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts index 85cb47a08..c2ef604bf 100644 --- a/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts +++ b/packages/trueforge-core/src/core/web-search/ParallelWebSearchProvider.ts @@ -15,8 +15,7 @@ const PARALLEL_SEARCH_MAX_CHARS_PER_RESULT = 500; const PARALLEL_SEARCH_MAX_CHARS_TOTAL = 4000; export interface ParallelWebSearchProviderOptions { - /** Parallel API key; omit or empty when the account allows unauthenticated use. */ - apiKey?: string; + apiKey: string; /** Parallel Search mode preset. Caller supplies the default (e.g. `turbo`). */ mode: ParallelSearchMode; } @@ -31,7 +30,7 @@ export class ParallelWebSearchProvider implements IWebSearchProvider { constructor(options: ParallelWebSearchProviderOptions) { this.#client = new Parallel({ - apiKey: options.apiKey ?? '', + apiKey: options.apiKey, fetch: ssrfFetch, }); this.#mode = options.mode; diff --git a/packages/trueforge-ui/src/containers/SettingsBuilder/ConfigureWebSearchForm.tsx b/packages/trueforge-ui/src/containers/SettingsBuilder/ConfigureWebSearchForm.tsx index 07c66e36f..f11834cab 100644 --- a/packages/trueforge-ui/src/containers/SettingsBuilder/ConfigureWebSearchForm.tsx +++ b/packages/trueforge-ui/src/containers/SettingsBuilder/ConfigureWebSearchForm.tsx @@ -6,9 +6,8 @@ import { auiInputClass } from '../../atoms/lib/inputClasses.js'; import { Button } from '../../atoms/primitives/Button.js'; import { CenteredModal } from '../../atoms/primitives/CenteredModal.js'; import { Icon } from '../../icons/Icon.js'; -import type { ParallelWebSearchMode, WebSearchProviderConfig } from '../../server/types.js'; -export type WebSearchConfigDraft = WebSearchProviderConfig & { +export type WebSearchConfigDraft = { apiKey: string; }; @@ -18,56 +17,34 @@ type ConfigureWebSearchFormProps = { onSave: (draft: WebSearchConfigDraft) => void | Promise; title: string; description?: string; - /** Prefills config fields; apiKey is never autofilled. */ - initialConfig?: WebSearchProviderConfig | null; /** When false (updates), empty apiKey means keep the existing key. */ requireApiKey?: boolean; busy?: boolean; error?: string | null; }; -const MODE_OPTIONS: Array<{ value: ParallelWebSearchMode; label: string }> = [ - { value: 'turbo', label: 'Turbo' }, - { value: 'fast', label: 'Fast' }, - { value: 'basic', label: 'Basic' }, - { value: 'advanced', label: 'Advanced' }, -]; - -const EMPTY_CONFIG: WebSearchProviderConfig = { - mode: 'turbo', -}; - const inputClassName = auiInputClass('h-11 shadow-sm'); -function isParallelWebSearchMode(value: string): value is ParallelWebSearchMode { - return MODE_OPTIONS.some(option => option.value === value); -} - const ConfigureWebSearchForm = ({ open, onOpenChange, onSave, title, description, - initialConfig = null, requireApiKey = true, busy = false, error, }: ConfigureWebSearchFormProps) => { - const [mode, setMode] = useState(EMPTY_CONFIG.mode); const [apiKey, setApiKey] = useState(''); const resetForm = () => { - setMode(EMPTY_CONFIG.mode); setApiKey(''); }; useEffect(() => { if (!open) return; - const config = initialConfig ?? EMPTY_CONFIG; - setMode(config.mode); setApiKey(''); - }, [open, initialConfig]); + }, [open]); const handleOpenChange = (nextOpen: boolean) => { if (!nextOpen) resetForm(); @@ -82,10 +59,7 @@ const ConfigureWebSearchForm = ({ if (!isValid || busy) return; try { - await onSave({ - mode, - apiKey: trimmedKey, - }); + await onSave({ apiKey: trimmedKey }); resetForm(); onOpenChange(false); } catch { @@ -129,30 +103,6 @@ const ConfigureWebSearchForm = ({ className={inputClassName} /> - -
- - -
diff --git a/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx b/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx index 8a94e53b7..f96d986f0 100644 --- a/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx +++ b/packages/trueforge-ui/src/containers/SettingsBuilder/WebSearchSettings.tsx @@ -5,32 +5,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Button } from '../../atoms/primitives/Button.js'; import { Icon } from '../../icons/Icon.js'; import { useCatalogServer } from '../../server/ServerContext.js'; -import type { - WebSearchProviderBase, - WebSearchProviderCatalogEntry, - WebSearchProviderConfig, -} from '../../server/types.js'; +import type { WebSearchProviderBase, WebSearchProviderCatalogEntry } from '../../server/types.js'; import { getErrorMessage } from '../../utils/getErrorMessage.js'; import { useToasterOptional } from '../ToasterContainer.js'; import ConfigureWebSearchForm, { type WebSearchConfigDraft } from './ConfigureWebSearchForm.js'; -const configFrom = ({ mode }: WebSearchProviderConfig): WebSearchProviderConfig => ({ mode }); - -const modeLabel = (mode: WebSearchProviderConfig['mode']): string => { - switch (mode) { - case 'turbo': - return 'Turbo'; - case 'fast': - return 'Fast'; - case 'basic': - return 'Basic'; - case 'advanced': - return 'Advanced'; - default: - return mode; - } -}; - const WebSearchSettings = () => { const { webSearchCatalog } = useCatalogServer(); const toaster = useToasterOptional(); @@ -78,11 +57,6 @@ const WebSearchSettings = () => { return catalog.filter(entry => !connectedCatalogIds.has(entry.id)); }, [catalog, providers, hasConfiguredProvider]); - const formInitialConfig = useMemo( - () => (updateProvider ? configFrom(updateProvider) : createEntry ? configFrom(createEntry) : null), - [updateProvider, createEntry], - ); - if (!webSearchCatalog) { return

Web search provider catalog is not available.

; } @@ -109,7 +83,6 @@ const WebSearchSettings = () => { catalogId: createEntry.id, name: createEntry.name, type: createEntry.type, - ...configFrom(draft), apiKey: draft.apiKey, }); }, setFormError); @@ -125,7 +98,6 @@ const WebSearchSettings = () => { await runMutation(async () => { await webSearchCatalog.updateWebSearchProvider({ id: updateProvider.id, - ...configFrom(draft), ...(draft.apiKey ? { apiKey: draft.apiKey } : {}), }); }, setFormError); @@ -181,7 +153,7 @@ const WebSearchSettings = () => {
{provider.name}
-

Mode · {modeLabel(provider.mode)}

+

Parallel web search

@@ -236,7 +208,7 @@ const WebSearchSettings = () => {
{entry.name}
-

Mode · {modeLabel(entry.mode)}

+

Parallel web search

@@ -277,7 +249,6 @@ const WebSearchSettings = () => { ? 'Update this web search provider. Leave API key blank to keep the existing key.' : 'Configure this web search provider. API key is never stored in the catalog.' } - initialConfig={formInitialConfig} requireApiKey={!isUpdate} busy={busy} error={formError} diff --git a/packages/trueforge-ui/src/index.ts b/packages/trueforge-ui/src/index.ts index d5cab5c1e..ad55ee35d 100644 --- a/packages/trueforge-ui/src/index.ts +++ b/packages/trueforge-ui/src/index.ts @@ -394,7 +394,6 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, - ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -440,7 +439,6 @@ export type { WebSearchCatalogServer, WebSearchProviderBase, WebSearchProviderCatalogEntry, - WebSearchProviderConfig, } from './server/types.js'; export { AgentCodeBlock } from './atoms/agent-details/AgentCodeBlock.js'; diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts index 60fd91937..7eff34801 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.ts @@ -12,7 +12,6 @@ import type { WebSearchCatalogServer, WebSearchProviderBase, WebSearchProviderCatalogEntry, - WebSearchProviderConfig, } from '../../../server/types.js'; export type UiWebSearchProvider = WebSearchProviderBase; @@ -28,20 +27,11 @@ function displayNameForType(type: string): string { return type; } -export function configFromHarness( - provider: TrueForgeApi.CatalogWebSearchProvider | TrueForgeApi.WebSearchProviderManifest, -): WebSearchProviderConfig { - return { - mode: provider.mode, - }; -} - export function toUiCatalogEntry(provider: TrueForgeApi.CatalogWebSearchProvider): UiWebSearchProviderCatalogEntry { return { id: provider.type, name: displayNameForType(provider.type), type: provider.type, - ...configFromHarness(provider), }; } @@ -51,7 +41,6 @@ export function toUiWebSearchProvider(provider: TrueForgeApi.WebSearchProviderMa name: displayNameForType(provider.type), catalogId: provider.type, isConnected: true, - ...configFromHarness(provider), }; } @@ -72,42 +61,31 @@ export function filterUiWebSearchProviders({ ); } -export function toHarnessManifest( - req: { - type: string; - apiKey: string; - } & WebSearchProviderConfig, -): TrueForgeApi.WebSearchProviderManifest { - if (req.type !== PARALLEL_TYPE) { - throw new Error(`Unsupported web-search provider type: ${req.type}`); +export function toHarnessManifest({ + type, + apiKey, +}: { + type: string; + apiKey: string; +}): TrueForgeApi.WebSearchProviderManifest { + if (type !== PARALLEL_TYPE) { + throw new Error(`Unsupported web-search provider type: ${type}`); } return { type: PARALLEL_TYPE, - mode: req.mode, - auth: { apiKey: req.apiKey }, + auth: { apiKey }, }; } -function findExistingProvider( - providers: TrueForgeApi.ConfiguredWebSearchProvider[], - id: string, -): TrueForgeApi.ConfiguredWebSearchProvider | undefined { - return providers.find(provider => provider.name === id || provider.manifest.type === id); -} - /** Settings web-search-catalog port for `createTrueForgeServer`. Delete omitted (no BE route). */ export function createWebSearchProviderCatalog(client: TrueForge): WebSearchCatalogServer { - async function resolveApiKey({ apiKey, id }: { apiKey: string | undefined; id: string }): Promise { + async function resolveApiKey(apiKey: string | undefined): Promise { const trimmed = apiKey?.trim(); if (trimmed !== undefined && trimmed !== '') { return trimmed; } - const existing = await client.settings.webSearchProviders.list(); - const match = findExistingProvider(existing.data, id); - if (match === undefined) { - throw new Error('API key is required'); - } - return match.manifest.auth.apiKey; + const existing = await client.settings.webSearchProviders.get(); + return existing.data.manifest.auth.apiKey; } return { @@ -116,28 +94,29 @@ export function createWebSearchProviderCatalog(client: TrueForge): WebSearchCata return body.data.map(toUiCatalogEntry); }, listWebSearchProviders: async req => { - const body = await client.settings.webSearchProviders.list(); - const providers = body.data.map(entry => toUiWebSearchProvider(entry.manifest)); + let providers: UiWebSearchProvider[]; + try { + const body = await client.settings.webSearchProviders.get(); + providers = [toUiWebSearchProvider(body.data.manifest)]; + } catch (err) { + if (err instanceof TrueForgeApi.NotFoundError) { + providers = []; + } else { + throw err; + } + } return filterUiWebSearchProviders({ providers, query: req?.query }); }, createWebSearchProvider: async req => { - const body = await client.settings.webSearchProviders.create({ - manifest: toHarnessManifest({ - type: req.type, - apiKey: req.apiKey, - mode: req.mode, - }), + const body = await client.settings.webSearchProviders.createOrUpdate({ + manifest: toHarnessManifest({ type: req.type, apiKey: req.apiKey }), }); return toUiWebSearchProvider(body.data.manifest); }, updateWebSearchProvider: async req => { - const apiKey = await resolveApiKey({ apiKey: req.apiKey, id: req.id }); + const apiKey = await resolveApiKey(req.apiKey); const body = await client.settings.webSearchProviders.createOrUpdate({ - manifest: toHarnessManifest({ - type: req.id, - apiKey, - mode: req.mode, - }), + manifest: toHarnessManifest({ type: PARALLEL_TYPE, apiKey }), }); return toUiWebSearchProvider(body.data.manifest); }, diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts index 4625ec2fc..bf0dfc84c 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts @@ -56,7 +56,6 @@ export { toHarnessManifest as toHarnessWebSearchManifest, toUiCatalogEntry as toUiWebSearchCatalogEntry, toUiWebSearchProvider, - configFromHarness as webSearchConfigFromHarness, } from './catalogs/webSearchProviderCatalog.js'; export { createHarnessChatServer, diff --git a/packages/trueforge-ui/src/server/types.ts b/packages/trueforge-ui/src/server/types.ts index 06b7957ec..28fa1e38a 100644 --- a/packages/trueforge-ui/src/server/types.ts +++ b/packages/trueforge-ui/src/server/types.ts @@ -84,7 +84,6 @@ export type { ModelSelection, ModelSelectorEntry, PageParams, - ParallelWebSearchMode, PermissionResourceType, PermissionsServer, PreviousTurnIdInput, @@ -136,8 +135,6 @@ export type { WebSearchBase, WebSearchCatalogEntry, WebSearchCatalogServer, - WebSearchConfig, WebSearchProviderBase, WebSearchProviderCatalogEntry, - WebSearchProviderConfig, } from '@truefoundry/trueforge-assistant-ui-runtime/server'; diff --git a/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx b/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx index de3c55924..6fbb54a5c 100644 --- a/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx +++ b/packages/trueforge-ui/test/containers/SettingsBuilder/WebSearchSettings.test.tsx @@ -26,7 +26,6 @@ const catalogEntry: WebSearchProviderCatalogEntry = { id: 'parallel', name: 'Parallel', type: 'parallel', - mode: 'turbo', }; function createFakeHost(initial: WebSearchProviderBase[] = []) { @@ -44,21 +43,12 @@ function createFakeHost(initial: WebSearchProviderBase[] = []) { name: req.name, catalogId: req.catalogId, isConnected: true, - mode: req.mode, }; providers = [...providers, provider]; return provider; }, updateWebSearchProvider: async (req: UpdateWebSearchProviderRequest) => { updated.push(req); - providers = providers.map(provider => - provider.id === req.id - ? { - ...provider, - mode: req.mode, - } - : provider, - ); const next = providers.find(provider => provider.id === req.id); if (next === undefined) { throw new Error(`Web search provider "${req.id}" not found`); @@ -79,7 +69,7 @@ function createFakeHost(initial: WebSearchProviderBase[] = []) { } describe('WebSearchSettings', () => { - it('autofills create form from catalog except apiKey', async () => { + it('requires apiKey on create', async () => { const host = createFakeHost(); const { wrapper: Wrapper } = host; render( @@ -95,17 +85,13 @@ describe('WebSearchSettings', () => { fireEvent.click(screen.getByRole('button', { name: 'Configure' })); await waitFor(() => { - expect(screen.getByLabelText('Mode')).toBeTruthy(); + expect(screen.getByLabelText('API key')).toBeTruthy(); }); - expect(screen.getByLabelText('Mode')).toHaveProperty('value', 'turbo'); expect(screen.getByLabelText('API key')).toHaveProperty('value', ''); fireEvent.change(screen.getByLabelText('API key'), { target: { value: 'par_secret' }, }); - fireEvent.change(screen.getByLabelText('Mode'), { - target: { value: 'fast' }, - }); fireEvent.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { @@ -115,18 +101,16 @@ describe('WebSearchSettings', () => { catalogId: 'parallel', name: 'Parallel', type: 'parallel', - mode: 'fast', apiKey: 'par_secret', }); }); - it('prefills update form and allows saving without re-entering apiKey', async () => { + it('allows update without re-entering apiKey', async () => { const existing: WebSearchProviderBase = { id: 'parallel', name: 'Parallel', catalogId: 'parallel', isConnected: true, - mode: 'basic', }; const host = createFakeHost([existing]); const { wrapper: Wrapper } = host; @@ -143,13 +127,9 @@ describe('WebSearchSettings', () => { fireEvent.click(screen.getByRole('button', { name: 'Update' })); await waitFor(() => { - expect(screen.getByLabelText('Mode')).toHaveProperty('value', 'basic'); + expect(screen.getByLabelText(/API key/)).toHaveProperty('value', ''); }); - expect(screen.getByLabelText(/API key/)).toHaveProperty('value', ''); - fireEvent.change(screen.getByLabelText('Mode'), { - target: { value: 'advanced' }, - }); fireEvent.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { @@ -157,7 +137,6 @@ describe('WebSearchSettings', () => { }); expect(host.updated[0]).toEqual({ id: 'parallel', - mode: 'advanced', }); expect(host.updated[0]).not.toHaveProperty('apiKey'); }); @@ -168,7 +147,6 @@ describe('WebSearchSettings', () => { name: 'Parallel', catalogId: 'parallel', isConnected: true, - mode: 'turbo', }; const host = createFakeHost([existing]); const { wrapper: Wrapper } = host; diff --git a/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts index 0c0a13113..47341e6b8 100644 --- a/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts +++ b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/webSearchProviderCatalog.test.ts @@ -2,7 +2,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'vitest'; import { - configFromHarness, filterUiWebSearchProviders, toHarnessManifest, toUiCatalogEntry, @@ -12,7 +11,6 @@ import { describe('webSearchProviderCatalog mappers', () => { const harnessCatalog = { type: 'parallel' as const, - mode: 'turbo' as const, }; const harnessConfigured = { @@ -25,7 +23,6 @@ describe('webSearchProviderCatalog mappers', () => { id: 'parallel', name: 'Parallel', type: 'parallel', - mode: 'turbo', }); }); @@ -35,7 +32,6 @@ describe('webSearchProviderCatalog mappers', () => { name: 'Parallel', catalogId: 'parallel', isConnected: true, - mode: 'turbo', }); assert.equal('auth' in toUiWebSearchProvider(harnessConfigured), false); assert.equal('apiKey' in toUiWebSearchProvider(harnessConfigured), false); @@ -48,26 +44,11 @@ describe('webSearchProviderCatalog mappers', () => { assert.deepEqual(filterUiWebSearchProviders({ providers: [provider], query: 'missing' }), []); }); - it('round-trips config fields into harness upsert body', () => { - assert.deepEqual( - toHarnessManifest({ - type: 'parallel', - apiKey: 'par_secret', - ...configFromHarness(harnessCatalog), - }), - harnessConfigured, - ); + it('round-trips apiKey into harness upsert body', () => { + assert.deepEqual(toHarnessManifest({ type: 'parallel', apiKey: 'par_secret' }), harnessConfigured); }); it('rejects unsupported web-search provider types', () => { - assert.throws( - () => - toHarnessManifest({ - type: 'other', - apiKey: 'x', - mode: 'turbo', - }), - /Unsupported web-search provider type/, - ); + assert.throws(() => toHarnessManifest({ type: 'other', apiKey: 'x' }), /Unsupported web-search provider type/); }); }); diff --git a/packages/trueforge/src/apis/webSearchProviders.ts b/packages/trueforge/src/apis/webSearchProviders.ts index 2deaf5409..5184d4dff 100644 --- a/packages/trueforge/src/apis/webSearchProviders.ts +++ b/packages/trueforge/src/apis/webSearchProviders.ts @@ -17,9 +17,6 @@ export interface WebSearchProvidersRouterDeps { } function redactWebSearchProvider(manifest: WebSearchProviderManifest): WebSearchProviderManifest { - if (!manifest.auth) { - return manifest; - } return { ...manifest, auth: { api_key: toRedactedSecretValue(manifest.auth.api_key) }, @@ -33,15 +30,12 @@ function resolveWebSearchProviderManifestForWrite({ incoming: WebSearchProviderManifest; existing: WebSearchProviderManifest | undefined; }): WebSearchProviderManifest { - if (!incoming.auth) { - return incoming; - } return { ...incoming, auth: { api_key: resolveStoredSecretValue({ incoming: incoming.auth.api_key, - existing: existing?.auth?.api_key, + existing: existing?.auth.api_key, }), }, }; diff --git a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts index 5af2a7658..1c6891471 100644 --- a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts +++ b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts @@ -1,2 +1,2 @@ // AUTO-GENERATED by scripts/generate-catalog.mjs — do not edit. -export const shippedWebSearchCatalogYaml = 'providers:\n - type: parallel\n' as const; +export const shippedWebSearchCatalogYaml = "providers:\n - type: parallel\n" as const; diff --git a/packages/trueforge/src/schemas/webSearchProvider.ts b/packages/trueforge/src/schemas/webSearchProvider.ts index 0bae26101..e94934c36 100644 --- a/packages/trueforge/src/schemas/webSearchProvider.ts +++ b/packages/trueforge/src/schemas/webSearchProvider.ts @@ -17,7 +17,7 @@ const ParallelWebSearchProviderAuthSchema = z export const ParallelWebSearchProviderSchema = z .object({ type: z.literal('parallel').describe('Parallel web-search provider.'), - auth: ParallelWebSearchProviderAuthSchema.optional(), + auth: ParallelWebSearchProviderAuthSchema, }) .strict(); diff --git a/packages/trueforge/src/websearch/providers.ts b/packages/trueforge/src/websearch/providers.ts index 3d63ca478..a9fef5a6c 100644 --- a/packages/trueforge/src/websearch/providers.ts +++ b/packages/trueforge/src/websearch/providers.ts @@ -24,6 +24,5 @@ export async function resolveWebSearchProvider({ return undefined; } const { manifest } = record; - const apiKey = manifest.auth?.api_key; - return new ParallelWebSearchProvider(apiKey ? { apiKey, mode: 'turbo' } : { mode: 'turbo' }); + return new ParallelWebSearchProvider({ apiKey: manifest.auth.api_key, mode: 'turbo' }); } diff --git a/packages/trueforge/tests/unit/apis/capabilities.test.ts b/packages/trueforge/tests/unit/apis/capabilities.test.ts index e7efc58a7..7f95232fe 100644 --- a/packages/trueforge/tests/unit/apis/capabilities.test.ts +++ b/packages/trueforge/tests/unit/apis/capabilities.test.ts @@ -121,7 +121,7 @@ describe('capabilities routers', () => { getProvider: () => Promise.resolve({ tenant_id: 'default', - manifest: { type: 'parallel' }, + manifest: { type: 'parallel', auth: { api_key: 'test-key' } }, created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-01T00:00:00.000Z', }), diff --git a/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts b/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts index 8323e9da2..ddb050c2c 100644 --- a/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/webSearchProviders.test.ts @@ -32,22 +32,13 @@ describe('settings web-search-providers router', () => { expect(await response.json()).toEqual({ error: { message: 'No web search provider configured' } }); }); - it('PUT without api_key upserts the singleton and GET returns it', async () => { + it('PUT without auth is rejected', async () => { const { router } = await createRouter(); const put = await router.request('/', putInit({ type: 'parallel' })); - expect(put.status).toBe(200); - expect(await put.json()).toEqual({ - data: { name: 'parallel', manifest: { type: 'parallel' } }, - }); - - const get = await router.request('/'); - expect(get.status).toBe(200); - expect(await get.json()).toEqual({ - data: { name: 'parallel', manifest: { type: 'parallel' } }, - }); + expect(put.status).toBe(400); }); - it('PUT with api_key redacts on response and second PUT replaces the same row', async () => { + it('PUT with api_key upserts the singleton, redacts on response, and second PUT replaces the same row', async () => { const { router, store } = await createRouter(); const apiKey = 'parallel-secret-key'; const created = await router.request('/', putInit({ type: 'parallel', auth: { api_key: apiKey } })); @@ -59,14 +50,23 @@ describe('settings web-search-providers router', () => { }, }); + const get = await router.request('/'); + expect(get.status).toBe(200); + expect(await get.json()).toEqual({ + data: { + name: 'parallel', + manifest: { type: 'parallel', auth: { api_key: toRedactedSecretValue(apiKey) } }, + }, + }); + const first = await store.getProvider('default'); - expect(first?.manifest.auth?.api_key).toBe(apiKey); + expect(first?.manifest.auth.api_key).toBe(apiKey); const rotated = await router.request('/', putInit({ type: 'parallel', auth: { api_key: 'parallel-rotated-key' } })); expect(rotated.status).toBe(200); const second = await store.getProvider('default'); - expect(second?.manifest.auth?.api_key).toBe('parallel-rotated-key'); + expect(second?.manifest.auth.api_key).toBe('parallel-rotated-key'); expect(second?.created_at).toBe(first?.created_at); }); @@ -80,6 +80,6 @@ describe('settings web-search-providers router', () => { putInit({ type: 'parallel', auth: { api_key: toRedactedSecretValue(apiKey) } }), ); expect(keep.status).toBe(200); - expect((await store.getProvider('default'))?.manifest.auth?.api_key).toBe(apiKey); + expect((await store.getProvider('default'))?.manifest.auth.api_key).toBe(apiKey); }); }); From ece542ed8f41ea041a6fc296fb1da3da3e2ea6d6 Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Tue, 22 Sep 2026 11:08:58 +0000 Subject: [PATCH 09/10] Regenerate OpenAPI document and SDKs --- .github/fern/openapi/openapi.json | 3 ++- docs/openapi.json | 3 ++- packages/trueforge-sdk/reference.md | 3 +++ .../webSearchProviders/client/Client.ts | 3 +++ .../UpdateWebSearchProviderRequest.ts | 3 +++ .../api/types/WebSearchProviderManifest.ts | 2 +- .../types/WebSearchProviderManifest.ts | 4 ++-- .../wire/settings/webSearchProviders.test.ts | 15 +++++++++--- python/trueforge_sdk/reference.md | 5 +++- .../settings/web_search_providers/client.py | 24 +++++++++++++++---- .../types/web_search_provider_manifest.py | 2 +- 11 files changed, 53 insertions(+), 14 deletions(-) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 7e66cc868..ec7345662 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -5870,7 +5870,8 @@ } }, "required": [ - "type" + "type", + "auth" ], "type": "object" } diff --git a/docs/openapi.json b/docs/openapi.json index 7e66cc868..ec7345662 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -5870,7 +5870,8 @@ } }, "required": [ - "type" + "type", + "auth" ], "type": "object" } diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 80105d47f..d4d1e893f 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -4129,6 +4129,9 @@ Upserts the single web search provider for this tenant. `auth.api_key`: real val ```typescript await client.settings.webSearchProviders.createOrUpdate({ manifest: { + auth: { + apiKey: "api_key" + }, type: "parallel" } }); diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts index 8caef57a5..c25d9a1a7 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/Client.ts @@ -146,6 +146,9 @@ export class WebSearchProvidersClient { * @example * await client.settings.webSearchProviders.createOrUpdate({ * manifest: { + * auth: { + * apiKey: "api_key" + * }, * type: "parallel" * } * }) diff --git a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts index 8223962b4..d24b84863 100644 --- a/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/settings/resources/webSearchProviders/client/requests/UpdateWebSearchProviderRequest.ts @@ -6,6 +6,9 @@ import type * as TrueForge from "../../../../../../index.js"; * @example * { * manifest: { + * auth: { + * apiKey: "api_key" + * }, * type: "parallel" * } * } diff --git a/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts index fbd77038e..fb70fa908 100644 --- a/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts +++ b/packages/trueforge-sdk/src/api/types/WebSearchProviderManifest.ts @@ -3,7 +3,7 @@ import type * as TrueForge from "../index.js"; export interface WebSearchProviderManifest { - auth?: TrueForge.ParallelWebSearchProviderAuth; + auth: TrueForge.ParallelWebSearchProviderAuth; /** Parallel web-search provider. */ type: "parallel"; } diff --git a/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts index b92930362..aba4ab2bc 100644 --- a/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts +++ b/packages/trueforge-sdk/src/serialization/types/WebSearchProviderManifest.ts @@ -9,13 +9,13 @@ export const WebSearchProviderManifest: core.serialization.ObjectSchema< serializers.WebSearchProviderManifest.Raw, TrueForge.WebSearchProviderManifest > = core.serialization.object({ - auth: ParallelWebSearchProviderAuth.optional(), + auth: ParallelWebSearchProviderAuth, type: core.serialization.stringLiteral("parallel"), }); export declare namespace WebSearchProviderManifest { export interface Raw { - auth?: ParallelWebSearchProviderAuth.Raw | null; + auth: ParallelWebSearchProviderAuth.Raw; type: "parallel"; } } diff --git a/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts index 5079d1883..0ae7ef59a 100644 --- a/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts +++ b/packages/trueforge-sdk/tests/wire/settings/webSearchProviders.test.ts @@ -95,7 +95,7 @@ describe("WebSearchProvidersClient", () => { test("create_or_update (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { type: "parallel" } }; + const rawRequestBody = { manifest: { auth: { api_key: "api_key" }, type: "parallel" } }; const rawResponseBody = { data: { manifest: { auth: { api_key: "api_key" }, type: "parallel" }, name: "name" }, }; @@ -111,6 +111,9 @@ describe("WebSearchProvidersClient", () => { const response = await client.settings.webSearchProviders.createOrUpdate({ manifest: { + auth: { + apiKey: "api_key", + }, type: "parallel", }, }); @@ -130,7 +133,7 @@ describe("WebSearchProvidersClient", () => { test("create_or_update (2)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { type: "parallel" } }; + const rawRequestBody = { manifest: { auth: { api_key: "x" }, type: "parallel" } }; const rawResponseBody = { error: { message: "message" } }; server @@ -145,6 +148,9 @@ describe("WebSearchProvidersClient", () => { await expect(async () => { return await client.settings.webSearchProviders.createOrUpdate({ manifest: { + auth: { + apiKey: "x", + }, type: "parallel", }, }); @@ -154,7 +160,7 @@ describe("WebSearchProvidersClient", () => { test("create_or_update (3)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { type: "parallel" } }; + const rawRequestBody = { manifest: { auth: { api_key: "x" }, type: "parallel" } }; const rawResponseBody = { error: { message: "message" } }; server @@ -169,6 +175,9 @@ describe("WebSearchProvidersClient", () => { await expect(async () => { return await client.settings.webSearchProviders.createOrUpdate({ manifest: { + auth: { + apiKey: "x", + }, type: "parallel", }, }); diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index da2d5d5df..e6f916a46 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -4843,7 +4843,7 @@ Upserts the single web search provider for this tenant. `auth.api_key`: real val
```python -from trueforge_sdk import TrueForge, WebSearchProviderManifest +from trueforge_sdk import TrueForge, WebSearchProviderManifest, ParallelWebSearchProviderAuth client = TrueForge( token="", @@ -4852,6 +4852,9 @@ client = TrueForge( client.settings.web_search_providers.create_or_update( manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), type="parallel", ), ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py index a27a27e9f..dd3f9d838 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/settings/web_search_providers/client.py @@ -74,14 +74,22 @@ def create_or_update( Examples -------- - from trueforge_sdk import TrueForge, WebSearchProviderManifest + from trueforge_sdk import ( + ParallelWebSearchProviderAuth, + TrueForge, + WebSearchProviderManifest, + ) client = TrueForge( token="YOUR_TOKEN", base_url="https://yourhost.com/path/to/api", ) client.settings.web_search_providers.create_or_update( - manifest=WebSearchProviderManifest(), + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + ), ) """ _response = self._raw_client.create_or_update(manifest=manifest, request_options=request_options) @@ -160,7 +168,11 @@ async def create_or_update( -------- import asyncio - from trueforge_sdk import AsyncTrueForge, WebSearchProviderManifest + from trueforge_sdk import ( + AsyncTrueForge, + ParallelWebSearchProviderAuth, + WebSearchProviderManifest, + ) client = AsyncTrueForge( token="YOUR_TOKEN", @@ -170,7 +182,11 @@ async def create_or_update( async def main() -> None: await client.settings.web_search_providers.create_or_update( - manifest=WebSearchProviderManifest(), + manifest=WebSearchProviderManifest( + auth=ParallelWebSearchProviderAuth( + api_key="api_key", + ), + ), ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py index 67250259b..6d19700a2 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/web_search_provider_manifest.py @@ -9,7 +9,7 @@ class WebSearchProviderManifest(UncheckedBaseModel): - auth: typing.Optional[ParallelWebSearchProviderAuth] = None + auth: ParallelWebSearchProviderAuth type: typing.Literal["parallel"] = pydantic.Field(default="parallel") """ Parallel web-search provider. From 51f2e4229e4456ec7894b6f0ba48507f425bec79 Mon Sep 17 00:00:00 2001 From: thesujai Date: Tue, 22 Sep 2026 16:51:26 +0530 Subject: [PATCH 10/10] fix: format web search catalog and match Turn metric label in test Co-authored-by: Cursor --- .../test/containers/AgentSessionTimelineContainer.test.tsx | 2 +- packages/trueforge/src/catalog/webSearchCatalog.gen.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trueforge-ui/test/containers/AgentSessionTimelineContainer.test.tsx b/packages/trueforge-ui/test/containers/AgentSessionTimelineContainer.test.tsx index ef0fc0470..059e8b29f 100644 --- a/packages/trueforge-ui/test/containers/AgentSessionTimelineContainer.test.tsx +++ b/packages/trueforge-ui/test/containers/AgentSessionTimelineContainer.test.tsx @@ -176,7 +176,7 @@ describe('AgentSessionTimelineContainer', () => { expect(await screen.findByText('timeline turns=1')).toBeInTheDocument(); expect(await screen.findByText('Turn 1')).toBeInTheDocument(); - expect(screen.getByText('Turns')).toBeInTheDocument(); + expect(screen.getByText('Turn')).toBeInTheDocument(); expect(screen.getByText('Duration')).toBeInTheDocument(); expect(await screen.findAllByRole('button', { name: 'Copy' })).not.toHaveLength(0); fireEvent.click(screen.getByRole('button', { name: 'timeline turns=1' })); diff --git a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts index 1c6891471..5af2a7658 100644 --- a/packages/trueforge/src/catalog/webSearchCatalog.gen.ts +++ b/packages/trueforge/src/catalog/webSearchCatalog.gen.ts @@ -1,2 +1,2 @@ // AUTO-GENERATED by scripts/generate-catalog.mjs — do not edit. -export const shippedWebSearchCatalogYaml = "providers:\n - type: parallel\n" as const; +export const shippedWebSearchCatalogYaml = 'providers:\n - type: parallel\n' as const;