fix: address web seam review findings
This commit is contained in:
@@ -38,6 +38,7 @@ dsh-tool-bash ← dsh-bash, dsh-tools (bash tool schemas)
|
||||
dsh-web ← dsh-llm (abstract web seam; search/fetch registries, WebError)
|
||||
dsh-web-search-exa ← dsh-web (Exa WebSearchProvider)
|
||||
dsh-web-search-perplexity ← dsh-web (Perplexity WebSearchProvider)
|
||||
dsh-web-search-deepseek ← dsh-web (DeepSeek native-web-search WebSearchProvider)
|
||||
dsh-web-fetch-local ← dsh-web (anonymous public HTTP(S) WebFetchProvider)
|
||||
dsh-tool-web ← dsh-web, dsh-tools, dsh-system-prompt (web tool schemas)
|
||||
dsh-llm-deepseek ← dsh-llm (DeepSeek adapter)
|
||||
@@ -80,6 +81,7 @@ The rule: **extension** plugins depend on interfaces, never on the concrete loop
|
||||
| `web/` | `web` | Abstract web seam (search/fetch provider registries + selection + vocabulary + `WebError`) | `ctx.web` |
|
||||
| `web-search-exa/` | `web` | Exa-backed `WebSearchProvider` | (registers on `ctx.web`) |
|
||||
| `web-search-perplexity/` | `web` | Perplexity-backed `WebSearchProvider` | (registers on `ctx.web`) |
|
||||
| `web-search-deepseek/` | `web` | DeepSeek-backed `WebSearchProvider` using native `web_search` through the Anthropic-compatible API | (registers on `ctx.web`) |
|
||||
| `web-fetch-local/` | `web` | Anonymous public HTTP(S) `WebFetchProvider` | (registers on `ctx.web`) |
|
||||
| `tool-web/` | `web` | Model-facing `web_search`/`web_fetch` tool schemas | (registers on `ctx.tools`) |
|
||||
| `llm-deepseek/` | `llm` | DeepSeek API adapter (hand-rolled fetch/SSE) | (registers on `ctx.llm`) |
|
||||
|
||||
@@ -7,6 +7,7 @@ The web access capability seam: an abstract web interface, search/fetch provider
|
||||
| `web/` | Abstract web seam (search/fetch provider registries + selection + vocabulary + `WebError`) | `ctx.web` |
|
||||
| `web-search-exa/` | Exa-backed `WebSearchProvider` | (registers on `ctx.web`) |
|
||||
| `web-search-perplexity/` | Perplexity-backed `WebSearchProvider` | (registers on `ctx.web`) |
|
||||
| `web-search-deepseek/` | DeepSeek-backed `WebSearchProvider` using native `web_search` through the Anthropic-compatible API | (registers on `ctx.web`) |
|
||||
| `web-fetch-local/` | Anonymous public HTTP(S) `WebFetchProvider` | (registers on `ctx.web`) |
|
||||
| `tool-web/` | Model-facing `web_search`/`web_fetch` tool schemas | (registers on `ctx.tools`) |
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ It reuses `$DEEPSEEK_API_KEY` (no new secret) but **not** `$DEEPSEEK_BASE_URL`:
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Use a separate env var such as `$DEEPSEEK_SEARCH_BASE_URL` when overriding it; do not reuse `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions LLM adapter. An unparseable value makes `status()` report `misconfigured`. |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic-format model name. |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` header value. |
|
||||
| `maxTokens` | `4096` | Upper bound on generated tokens for the Messages request. |
|
||||
| `maxUses` | `5` | Maximum `web_search` server-tool uses per request. |
|
||||
| `maxTokens` | `4096` | Positive-integer upper bound on generated tokens for the Messages request. |
|
||||
| `maxUses` | `5` | Positive-integer maximum `web_search` server-tool uses per request. |
|
||||
|
||||
```yaml
|
||||
- id: web-search-deepseek
|
||||
|
||||
@@ -64,18 +64,20 @@ export const Config: z<Config> = z.object({
|
||||
baseURL: z.string(),
|
||||
model: z.string(),
|
||||
apiVersion: z.string(),
|
||||
maxTokens: z.natural(),
|
||||
maxUses: z.natural(),
|
||||
maxTokens: z.number().step(1).min(1),
|
||||
maxUses: z.number().step(1).min(1),
|
||||
})
|
||||
|
||||
/** Register the DeepSeek search provider with `ctx.web`. */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const maxTokens = config.maxTokens ?? DEEPSEEK_DEFAULT_MAX_TOKENS
|
||||
const maxUses = config.maxUses ?? DEEPSEEK_DEFAULT_MAX_USES
|
||||
ctx.web.registerSearchProvider(new DeepSeekSearchProvider({
|
||||
apiKey: config.apiKey ?? process.env.DEEPSEEK_API_KEY ?? '',
|
||||
baseURL: config.baseURL ?? DEEPSEEK_DEFAULT_BASE_URL,
|
||||
model: config.model ?? DEEPSEEK_DEFAULT_MODEL,
|
||||
apiVersion: config.apiVersion ?? DEEPSEEK_DEFAULT_API_VERSION,
|
||||
maxTokens: config.maxTokens ?? DEEPSEEK_DEFAULT_MAX_TOKENS,
|
||||
maxUses: config.maxUses ?? DEEPSEEK_DEFAULT_MAX_USES,
|
||||
maxTokens,
|
||||
maxUses,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
status(): WebProviderStatus {
|
||||
if (this.options.apiKey.length === 0) return { available: false, reason: 'missing-credential' }
|
||||
if (!URL.canParse(this.options.baseURL)) return { available: false, reason: 'misconfigured' }
|
||||
if (!isPositiveInteger(this.options.maxTokens) || !isPositiveInteger(this.options.maxUses)) return { available: false, reason: 'misconfigured' }
|
||||
return { available: true }
|
||||
}
|
||||
|
||||
@@ -215,3 +216,8 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === 'AbortError'
|
||||
}
|
||||
|
||||
/** True for DeepSeek request limits that can be sent to the Messages API. */
|
||||
function isPositiveInteger(value: number): boolean {
|
||||
return Number.isInteger(value) && value > 0
|
||||
}
|
||||
|
||||
@@ -152,6 +152,15 @@ describe('DeepSeekSearchProvider status', () => {
|
||||
expect(new DeepSeekSearchProvider({ ...options, baseURL: 'not a url' }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
})
|
||||
|
||||
it('is misconfigured when request limits are not positive integers', () => {
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxTokens: 0 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxUses: 0 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxUses: 1.5 }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeepSeekSearchProvider request mapping', () => {
|
||||
@@ -263,6 +272,27 @@ describe('web-search-deepseek plugin registration', () => {
|
||||
expect(ctx.web.searchStatus()).toEqual({ available: false, reason: 'configured-missing' })
|
||||
})
|
||||
|
||||
it('rejects maxTokens: 0 at plugin construction', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
|
||||
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxTokens: 0 }))
|
||||
.rejects.toThrow(/maxTokens expected number >= 1/)
|
||||
})
|
||||
|
||||
it('rejects maxUses: 0 at plugin construction', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
|
||||
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxUses: 0 }))
|
||||
.rejects.toThrow(/maxUses expected number >= 1/)
|
||||
})
|
||||
|
||||
it('rejects a fractional maxUses at plugin construction', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
|
||||
await expect(ctx.plugin(deepseekPlugin, { apiKey: 'ds-key', maxUses: 1.5 }))
|
||||
.rejects.toThrow(/maxUses expected number multiple of 1/)
|
||||
})
|
||||
|
||||
it('has no default export (namespace plugin export shape)', () => {
|
||||
expect('default' in deepseekPlugin).toBe(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user