The Web search switch selects the DeepSeek Search and Fetch APIs together, and both reuse the DeepSeek chat key by default. Neither provider answered readiness, so a step without a usable key was offered web_search and web_fetch whose every call failed, and a conversation on another provider's model or behind a gateway sent the chat key to DeepSeek. dsh-web gives WebFetchProvider an optional ready(llmProvider) and adds ctx.web.fetchReady(), which asks only the provider fetch() selects, as searchReady() does for search. tool-web registers web_fetch with a step activation condition on fetchReady(), so a step that cannot fetch leaves the tool out of declarations, guidance, and PTC bindings and refuses a call to it as UNKNOWN_TOOL; without web_fetch the search guidance uses its search-only text. The shipped HTTP fetch provider has no ready() and stays admitted. web-deepseek-api applies the API-key rules of Messages search to both APIs: without a literal apiKey, an apiKeyEnv other than DEEPSEEK_API_KEY, or a baseURL other than the default, the chat key goes only to the DeepSeek routes and only to an API base on the official chat route's origin. The refusal sits in the key's only resolver, and both providers' ready() answers presence in the order an operation resolves credentials. The transport repeats these checks so the package still does not depend on dsh-web-search-deepseek.
104 lines
5.1 KiB
TypeScript
104 lines
5.1 KiB
TypeScript
/**
|
|
* Model-facing `web_search` and `web_fetch` tools over `ctx.web`. This package owns schemas,
|
|
* validation, prompt guidance, limits, and presentation, never concrete providers. Enablement
|
|
* controls tool registration; an enabled tool remains visible when its provider is unavailable
|
|
* and fails with a structured error at execution time. A model step does not admit
|
|
* `web_search` or `web_fetch` while `ctx.web.searchReady()` or `ctx.web.fetchReady()`
|
|
* reports that the selected provider cannot authenticate for that step's chat route.
|
|
* @module @deepseek-ai/dsh-tool-web
|
|
*/
|
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import type {} from '@deepseek-ai/dsh-web'
|
|
import { applyWebSearchTool, WEB_SEARCH_MAX_QUERIES, WEB_SEARCH_MAX_RESULTS, WEB_SEARCH_SUMMARY_MAX_CHARS } from './search.ts'
|
|
import { applyWebFetchTool } from './fetch.ts'
|
|
|
|
export { WEB_SEARCH_MAX_QUERIES, WEB_SEARCH_MAX_RESULTS, applyWebSearchTool, formatSearchOutput, presentSearchCall, presentSearchResult, searchMetaFromValue, searchMetaFromResult } from './search.ts'
|
|
export type { WebSearchMeta } from './search.ts'
|
|
export { applyWebFetchTool, formatFetchOutput, parseFetchArgs, presentFetchCall, presentFetchResult, fetchMetaFromValue, fetchMetaFromResult } from './fetch.ts'
|
|
export type { WebFetchMeta } from './fetch.ts'
|
|
|
|
/** Cordis plugin name used by loader diagnostics. */
|
|
export const name = 'tool-web'
|
|
|
|
/** Services required by the web tool suite. */
|
|
export const inject = ['tools', 'web', 'systemPrompt']
|
|
|
|
/** Default cooperative tool-call timeout budget (ms) for the web tools. */
|
|
export const DEFAULT_WEB_TOOL_TIMEOUT_MS = 30_000
|
|
|
|
/**
|
|
* Default cap on one `web_fetch` output and on source characters converted
|
|
* synchronously. This leaves headroom above the local provider's default
|
|
* 100,000-character body cap while bounding custom providers and rendered output.
|
|
*/
|
|
export const DEFAULT_FETCH_MAX_OUTPUT_CHARS = 200_000
|
|
|
|
/** Plugin config: which web tools to register, search bounds, per-tool budgets, and the fetch output cap. */
|
|
export interface Config {
|
|
/** Register `web_search`. Defaults to true. */
|
|
search?: boolean
|
|
/** Register `web_fetch`. Defaults to true. */
|
|
fetch?: boolean
|
|
/** Upper bound on sources returned by one `web_search` call. */
|
|
searchMaxResults?: number
|
|
/** Upper bound on queries accepted by one `web_search` call. */
|
|
searchMaxQueries?: number
|
|
/** Cap on each source summary in `web_search` output, in characters. Defaults to 800. */
|
|
searchSummaryMaxChars?: number
|
|
/** Cooperative timeout budget (ms) for `web_fetch`. Defaults to 30000. */
|
|
fetchTimeoutMs?: number
|
|
/** Cooperative timeout budget (ms) for `web_search`. Defaults to 30000. */
|
|
searchTimeoutMs?: number
|
|
/** Cap on source characters converted and complete `web_fetch` output characters. Defaults to 200000. */
|
|
fetchMaxOutputChars?: number
|
|
}
|
|
|
|
export const Config: z<Config> = z.object({
|
|
search: z.boolean().default(true),
|
|
fetch: z.boolean().default(true),
|
|
searchMaxResults: z.number().default(WEB_SEARCH_MAX_RESULTS),
|
|
searchMaxQueries: z.number().default(WEB_SEARCH_MAX_QUERIES),
|
|
searchSummaryMaxChars: z.number().default(WEB_SEARCH_SUMMARY_MAX_CHARS),
|
|
fetchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
|
|
searchTimeoutMs: z.number().default(DEFAULT_WEB_TOOL_TIMEOUT_MS),
|
|
fetchMaxOutputChars: z.number().default(DEFAULT_FETCH_MAX_OUTPUT_CHARS),
|
|
})
|
|
|
|
/** Complete config after schemastery applies every field default. */
|
|
type ResolvedConfig = Required<Config>
|
|
|
|
/** Configured count, timeout, and character caps must be positive integers. */
|
|
function assertPositiveInteger(name: string, value: number): void {
|
|
if (!Number.isInteger(value) || value < 1) {
|
|
throw new Error(`tool-web: ${name} must be a positive integer`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register the enabled web tools. `search`/`fetch` default to true; a product
|
|
* that wants only one disables the other in config. Each tool's cooperative
|
|
* timeout budget (`fetchTimeoutMs`/`searchTimeoutMs`, default 30000) is resolved
|
|
* here and attached to the tool as `ToolDefinition.timeoutMs` for
|
|
* `@deepseek-ai/dsh-tool-call-timeout-policy` to enforce. The tools' disposers are
|
|
* fiber-scoped (the effect-based registries clean up on dispose), so no manual
|
|
* teardown is needed.
|
|
*/
|
|
export function apply(ctx: Context, config: Config): void {
|
|
// schemastery (Config) has already filled every defaulted field.
|
|
const resolved = config as ResolvedConfig
|
|
assertPositiveInteger('searchMaxResults', resolved.searchMaxResults)
|
|
assertPositiveInteger('searchMaxQueries', resolved.searchMaxQueries)
|
|
assertPositiveInteger('searchSummaryMaxChars', resolved.searchSummaryMaxChars)
|
|
assertPositiveInteger('fetchTimeoutMs', resolved.fetchTimeoutMs)
|
|
assertPositiveInteger('searchTimeoutMs', resolved.searchTimeoutMs)
|
|
assertPositiveInteger('fetchMaxOutputChars', resolved.fetchMaxOutputChars)
|
|
if (resolved.search) {
|
|
applyWebSearchTool(
|
|
ctx, resolved.searchMaxResults, resolved.searchMaxQueries, resolved.searchSummaryMaxChars, resolved.searchTimeoutMs, resolved.fetch,
|
|
)
|
|
}
|
|
if (resolved.fetch) applyWebFetchTool(ctx, resolved.fetchTimeoutMs, resolved.fetchMaxOutputChars)
|
|
}
|