refactor: prune unused web seam fields
This commit is contained in:
@@ -16,8 +16,8 @@ It reuses `$DEEPSEEK_API_KEY` (no new secret) but **not** `$DEEPSEEK_BASE_URL`:
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `apiKey` | `$DEEPSEEK_API_KEY` | DeepSeek API key. Empty/absent → provider `status()` reports `missing-credential`. Sent as both `x-api-key` and `Authorization: Bearer` (official vs Anthropic-compatible proxy). |
|
||||
| `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`. |
|
||||
| `apiKey` | `$DEEPSEEK_API_KEY` | DeepSeek API key. Empty/absent makes the provider unavailable. Sent as both `x-api-key` and `Authorization: Bearer` (official vs Anthropic-compatible proxy). |
|
||||
| `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 the provider unavailable. |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic-format model name. |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` header value. |
|
||||
| `maxTokens` | `4096` | Positive-integer upper bound on generated tokens for the Messages request. |
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import { WebError } from '@deepseek-ai/dsh-web'
|
||||
import type {
|
||||
WebProviderStatus,
|
||||
WebSearchProvider,
|
||||
WebSearchRequest,
|
||||
WebSearchResult,
|
||||
@@ -64,7 +63,7 @@ const USER_AGENT = 'deepseek-harness/0.0.1'
|
||||
|
||||
/** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */
|
||||
export interface DeepSeekSearchProviderOptions {
|
||||
/** DeepSeek API key. Empty/absent → `status()` reports `missing-credential`. */
|
||||
/** DeepSeek API key. Empty/absent makes the provider unavailable. */
|
||||
apiKey: string
|
||||
/** Endpoint base; `/messages` is appended. */
|
||||
baseURL: string
|
||||
@@ -111,11 +110,10 @@ export function citationSnippets(blocks: readonly ContentBlock[]): Map<string, s
|
||||
* block is present — native search did not trigger, and prose-scraping is not a
|
||||
* fallback.
|
||||
*
|
||||
* @param query - the original request query, echoed on the result.
|
||||
* @param response - the parsed Messages response body.
|
||||
* @returns the normalized result with deduped, snippet-joined sources.
|
||||
*/
|
||||
export function mapAnthropicResponse(query: string, response: AnthropicResponse): WebSearchResult {
|
||||
export function mapAnthropicResponse(response: AnthropicResponse): WebSearchResult {
|
||||
const blocks = response.content ?? []
|
||||
const resultBlocks = blocks.filter(
|
||||
(block): block is WebSearchToolResultBlock => block.type === 'web_search_tool_result',
|
||||
@@ -143,7 +141,7 @@ export function mapAnthropicResponse(query: string, response: AnthropicResponse)
|
||||
})
|
||||
}
|
||||
}
|
||||
return { providerId: DEEPSEEK_PROVIDER_ID, query, sources, truncated: false }
|
||||
return { sources, truncated: false }
|
||||
}
|
||||
|
||||
/** The DeepSeek-backed search provider. */
|
||||
@@ -152,14 +150,14 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
|
||||
constructor(private readonly options: DeepSeekSearchProviderOptions) {}
|
||||
|
||||
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 }
|
||||
available(): boolean {
|
||||
return this.options.apiKey.length > 0
|
||||
&& URL.canParse(this.options.baseURL)
|
||||
&& isPositiveInteger(this.options.maxTokens)
|
||||
&& isPositiveInteger(this.options.maxUses)
|
||||
}
|
||||
|
||||
async search(request: WebSearchRequest, exec?: { readonly signal?: AbortSignal }): Promise<WebSearchResult> {
|
||||
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${this.options.baseURL}/messages`, {
|
||||
@@ -183,7 +181,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
}],
|
||||
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: this.options.maxUses }],
|
||||
}),
|
||||
...exec?.signal ? { signal: exec.signal } : {},
|
||||
...signal !== undefined ? { signal } : {},
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (isAbortError(error)) throw new WebError('DeepSeek search aborted', 'WEB_ABORTED', { cause: error })
|
||||
@@ -211,7 +209,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
|
||||
try {
|
||||
const payload = await response.json() as AnthropicResponse
|
||||
return mapAnthropicResponse(request.query, payload)
|
||||
return mapAnthropicResponse(payload)
|
||||
} catch (error: unknown) {
|
||||
if (isAbortError(error)) throw new WebError('DeepSeek search aborted', 'WEB_ABORTED', { cause: error })
|
||||
if (error instanceof WebError) throw error
|
||||
|
||||
@@ -29,7 +29,6 @@ maybe('DeepSeekSearchProvider real API', () => {
|
||||
maxUses: DEEPSEEK_DEFAULT_MAX_USES,
|
||||
})
|
||||
const result = await provider.search({ query: 'What is the DeepSeek Harness SDK?', maxResults: 5 })
|
||||
expect(result.providerId).toBe('deepseek')
|
||||
expect(result.sources.length).toBeGreaterThan(0)
|
||||
for (const source of result.sources) expect(source.url).toMatch(/^https?:\/\//)
|
||||
}, 60_000)
|
||||
|
||||
@@ -64,10 +64,8 @@ describe('citationSnippets', () => {
|
||||
|
||||
describe('mapAnthropicResponse', () => {
|
||||
it('joins result items to citation snippets and maps page_age to publishedAt', () => {
|
||||
const result = mapAnthropicResponse('q', searchResponse())
|
||||
const result = mapAnthropicResponse(searchResponse())
|
||||
expect(result).toEqual({
|
||||
providerId: DEEPSEEK_PROVIDER_ID,
|
||||
query: 'q',
|
||||
sources: [
|
||||
{ url: 'https://a.test', title: 'A', snippet: 'excerpt for A', publishedAt: '2026-02-02' },
|
||||
{ url: 'https://b.test', title: 'B' },
|
||||
@@ -77,7 +75,7 @@ describe('mapAnthropicResponse', () => {
|
||||
})
|
||||
|
||||
it('dedupes repeated urls across result blocks (first wins)', () => {
|
||||
const result = mapAnthropicResponse('q', {
|
||||
const result = mapAnthropicResponse({
|
||||
content: [
|
||||
{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'first' }] },
|
||||
{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'second' }] },
|
||||
@@ -87,7 +85,7 @@ describe('mapAnthropicResponse', () => {
|
||||
})
|
||||
|
||||
it('skips non-result items and items with an empty url', () => {
|
||||
const result = mapAnthropicResponse('q', {
|
||||
const result = mapAnthropicResponse({
|
||||
content: [{
|
||||
type: 'web_search_tool_result',
|
||||
content: [
|
||||
@@ -101,14 +99,14 @@ describe('mapAnthropicResponse', () => {
|
||||
})
|
||||
|
||||
it('omits optional fields when absent or empty', () => {
|
||||
const result = mapAnthropicResponse('q', {
|
||||
const result = mapAnthropicResponse({
|
||||
content: [{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: '', page_age: '' }] }],
|
||||
})
|
||||
expect(result.sources).toEqual([{ url: 'https://a.test' }])
|
||||
})
|
||||
|
||||
it('tolerates a text block with no citations', () => {
|
||||
const result = mapAnthropicResponse('q', {
|
||||
const result = mapAnthropicResponse({
|
||||
content: [
|
||||
{ type: 'text', text: 'no citations here' },
|
||||
{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test', title: 'A' }] },
|
||||
@@ -118,7 +116,7 @@ describe('mapAnthropicResponse', () => {
|
||||
})
|
||||
|
||||
it('tolerates a result block with no content array', () => {
|
||||
const result = mapAnthropicResponse('q', {
|
||||
const result = mapAnthropicResponse({
|
||||
content: [
|
||||
{ type: 'web_search_tool_result' },
|
||||
{ type: 'web_search_tool_result', content: [{ type: 'web_search_result', url: 'https://a.test' }] },
|
||||
@@ -128,38 +126,33 @@ describe('mapAnthropicResponse', () => {
|
||||
})
|
||||
|
||||
it('throws WEB_PROVIDER_ERROR (strict mode) when no result block is present', () => {
|
||||
expect(() => mapAnthropicResponse('q', { content: [{ type: 'text', text: 'just prose, no search' }] }))
|
||||
expect(() => mapAnthropicResponse({ content: [{ type: 'text', text: 'just prose, no search' }] }))
|
||||
.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_ERROR' }))
|
||||
})
|
||||
|
||||
it('throws WEB_PROVIDER_ERROR when content is absent entirely', () => {
|
||||
expect(() => mapAnthropicResponse('q', {}))
|
||||
expect(() => mapAnthropicResponse({}))
|
||||
.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_ERROR' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeepSeekSearchProvider status', () => {
|
||||
describe('DeepSeekSearchProvider availability', () => {
|
||||
it('is unavailable without a key', () => {
|
||||
expect(new DeepSeekSearchProvider({ ...options, apiKey: '' }).status())
|
||||
.toEqual({ available: false, reason: 'missing-credential' })
|
||||
expect(new DeepSeekSearchProvider({ ...options, apiKey: '' }).available()).toBe(false)
|
||||
})
|
||||
|
||||
it('is available with a key', () => {
|
||||
expect(new DeepSeekSearchProvider(options).status()).toEqual({ available: true })
|
||||
expect(new DeepSeekSearchProvider(options).available()).toBe(true)
|
||||
})
|
||||
|
||||
it('is misconfigured when the base URL is unparseable', () => {
|
||||
expect(new DeepSeekSearchProvider({ ...options, baseURL: 'not a url' }).status())
|
||||
.toEqual({ available: false, reason: 'misconfigured' })
|
||||
expect(new DeepSeekSearchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false)
|
||||
})
|
||||
|
||||
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' })
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxTokens: 0 }).available()).toBe(false)
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxUses: 0 }).available()).toBe(false)
|
||||
expect(new DeepSeekSearchProvider({ ...options, maxUses: 1.5 }).available()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -186,7 +179,7 @@ describe('DeepSeekSearchProvider request mapping', () => {
|
||||
const fetchMock = vi.fn(async () => jsonResponse(searchResponse()))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const controller = new AbortController()
|
||||
await new DeepSeekSearchProvider(options).search({ query: 'q' }, { signal: controller.signal })
|
||||
await new DeepSeekSearchProvider(options).search({ query: 'q' }, controller.signal)
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
|
||||
expect(init.signal).toBe(controller.signal)
|
||||
})
|
||||
@@ -268,7 +261,7 @@ describe('web-search-deepseek plugin registration', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(WebService, { searchProvider: DEEPSEEK_PROVIDER_ID })
|
||||
const fiber = await ctx.plugin(deepseekPlugin, { apiKey: 'ds-key' })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: DEEPSEEK_PROVIDER_ID })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ truncated: false })
|
||||
await fiber.dispose()
|
||||
await expect(ctx.web.search({ query: 'q' }))
|
||||
.rejects.toThrow(expect.objectContaining({ code: 'WEB_PROVIDER_CONFIGURED_MISSING' }))
|
||||
@@ -324,7 +317,7 @@ describe('web-search-deepseek plugin registration', () => {
|
||||
const unwrapped = loader.unwrapExports(deepseekPlugin) as Parameters<Context['plugin']>[0]
|
||||
// A collapsed export shape (dropped inject) would throw "without inject" here.
|
||||
const fiber = await ctx.plugin(unwrapped, { apiKey: 'ds-key' })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ providerId: DEEPSEEK_PROVIDER_ID })
|
||||
await expect(ctx.web.search({ query: 'q' })).resolves.toMatchObject({ truncated: false })
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user