|
|
@@ -178,41 +178,42 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
|
|
readonly id = DEEPSEEK_PROVIDER_ID
|
|
|
|
|
|
/**
|
|
|
- * @param resolveOptions - the options for the NEXT operation. A thunk rather
|
|
|
- * than a value because the plugin's settings section can change between
|
|
|
- * searches, and re-registering the provider to carry a new endpoint would
|
|
|
- * make the seam's selection observable to the user as a flicker.
|
|
|
+ * @param resolveOptions - the options for the NEXT operation, snapshotted
|
|
|
+ * once at each operation's entry so one search never mixes two sections. A
|
|
|
+ * thunk rather than a value because the plugin's settings section can change
|
|
|
+ * between searches, and re-registering the provider to carry a new endpoint
|
|
|
+ * would make the seam's selection observable to the user as a flicker.
|
|
|
*/
|
|
|
constructor(private readonly resolveOptions: () => DeepSeekSearchProviderOptions) {}
|
|
|
|
|
|
- /** Options resolved per read, so a committed settings change reaches the next search. */
|
|
|
- private get options(): DeepSeekSearchProviderOptions {
|
|
|
- return this.resolveOptions()
|
|
|
- }
|
|
|
-
|
|
|
available(): boolean {
|
|
|
- return ((this.options.apiKey?.length ?? 0) > 0 || this.options.resolveApiKey !== undefined)
|
|
|
- && URL.canParse(this.options.baseURL)
|
|
|
- && isPositiveInteger(this.options.maxTokens)
|
|
|
- && isPositiveInteger(this.options.maxUses)
|
|
|
+ const options = this.resolveOptions()
|
|
|
+ return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== undefined)
|
|
|
+ && URL.canParse(options.baseURL)
|
|
|
+ && isPositiveInteger(options.maxTokens)
|
|
|
+ && isPositiveInteger(options.maxUses)
|
|
|
}
|
|
|
|
|
|
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
|
|
- const apiKey = await this.apiKey(signal)
|
|
|
+ // One snapshot for the whole operation: credential resolution awaits, and a
|
|
|
+ // settings write landing inside that await must not send the key resolved
|
|
|
+ // from the old section to the endpoint named by the new one.
|
|
|
+ const options = this.resolveOptions()
|
|
|
+ const apiKey = await this.apiKey(options, signal)
|
|
|
throwIfSearchAborted(signal)
|
|
|
- const endpoint = `${this.options.baseURL}/messages`
|
|
|
+ const endpoint = `${options.baseURL}/messages`
|
|
|
const body: DeepSeekSearchLlmRequest['body'] = {
|
|
|
- model: this.options.model,
|
|
|
- max_tokens: this.options.maxTokens,
|
|
|
+ model: options.model,
|
|
|
+ max_tokens: options.maxTokens,
|
|
|
messages: [{
|
|
|
role: 'user',
|
|
|
content: [{ type: 'text', text: `Perform a web search for the query: ${request.query}` }],
|
|
|
}],
|
|
|
- tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: this.options.maxUses }],
|
|
|
+ tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: options.maxUses }],
|
|
|
}
|
|
|
- this.options.recordRequest?.({
|
|
|
+ options.recordRequest?.({
|
|
|
endpoint,
|
|
|
- apiVersion: this.options.apiVersion,
|
|
|
+ apiVersion: options.apiVersion,
|
|
|
body,
|
|
|
})
|
|
|
throwIfSearchAborted(signal)
|
|
|
@@ -226,7 +227,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
|
|
// may expect `Authorization: Bearer` — send both so either resolves.
|
|
|
'x-api-key': apiKey,
|
|
|
'authorization': `Bearer ${apiKey}`,
|
|
|
- 'anthropic-version': this.options.apiVersion,
|
|
|
+ 'anthropic-version': options.apiVersion,
|
|
|
'content-type': 'application/json',
|
|
|
'accept': 'application/json',
|
|
|
'user-agent': USER_AGENT,
|
|
|
@@ -268,13 +269,18 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** Resolve one operation's credential without retaining it on the provider. */
|
|
|
- private async apiKey(signal?: AbortSignal): Promise<string> {
|
|
|
+ /**
|
|
|
+ * Resolve one operation's credential without retaining it on the provider.
|
|
|
+ * @param options - the caller's snapshot, so the key and the endpoint it is sent to come from one section.
|
|
|
+ * @param signal - abort signal for the surrounding search.
|
|
|
+ * @returns the resolved key.
|
|
|
+ */
|
|
|
+ private async apiKey(options: DeepSeekSearchProviderOptions, signal?: AbortSignal): Promise<string> {
|
|
|
throwIfSearchAborted(signal)
|
|
|
- if (this.options.apiKey !== undefined && this.options.apiKey.length > 0) return this.options.apiKey
|
|
|
+ if (options.apiKey !== undefined && options.apiKey.length > 0) return options.apiKey
|
|
|
let resolved: string | undefined
|
|
|
try {
|
|
|
- resolved = await abortable(this.options.resolveApiKey?.() ?? Promise.resolve(undefined), signal)
|
|
|
+ resolved = await abortable(options.resolveApiKey?.() ?? Promise.resolve(undefined), signal)
|
|
|
} catch (error: unknown) {
|
|
|
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error)
|
|
|
throw new WebError(
|
|
|
@@ -284,7 +290,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
|
|
)
|
|
|
}
|
|
|
if (resolved !== undefined && resolved.length > 0) return resolved
|
|
|
- const ref = this.options.apiKeyEnv ?? 'DEEPSEEK_API_KEY'
|
|
|
+ const ref = options.apiKeyEnv ?? 'DEEPSEEK_API_KEY'
|
|
|
throw new WebError(
|
|
|
`DeepSeek search has no API key for "${ref}"; store it through the credentials service`
|
|
|
+ ' (the web Models page writes it), export it in the launching environment, or set a literal'
|