feat(image-recognition): independent key/url/model defaults
Make the HTTP vision provider's configuration independent of the main chat model: the API key default moves from DEEPSEEK_API_KEY to IMAGE_RECOGNITION_API_KEY, and the URL and model get image-recognition-specific defaults (Aliyun DashScope compatible-mode and qwen3-vl-flash) instead of the chat model's deepseek-v4-flash. The provider fails loud when no model is configured. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,10 @@ URL and returns the recognized text.
|
|||||||
|
|
||||||
| Key | Type | Meaning |
|
| Key | Type | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `baseURL` | string | Endpoint base; `/chat/completions` is appended. Blank inherits `$DSH_IMAGE_RECOGNITION_BASE_URL`, else unavailable. |
|
| `baseURL` | string | Endpoint base; `/chat/completions` is appended. Defaults to `https://dashscope.aliyuncs.com/compatible-mode/v1` (Aliyun DashScope compatible-mode); `$DSH_IMAGE_RECOGNITION_BASE_URL` overrides. |
|
||||||
| `apiKey` | string (secret) | Literal key; prefer `apiKeyEnv`. |
|
| `apiKey` | string (secret) | Literal key; prefer `apiKeyEnv`. |
|
||||||
| `apiKeyEnv` | string (credential-ref) | Credential reference resolved per recognition; defaults to `DEEPSEEK_API_KEY`. |
|
| `apiKeyEnv` | string (credential-ref) | Credential reference resolved per recognition; defaults to `IMAGE_RECOGNITION_API_KEY` (distinct from the main chat model's `DEEPSEEK_API_KEY`). |
|
||||||
| `model` | string | Vision model name; defaults to `deepseek-v4-flash`. |
|
| `model` | string | Vision model name; defaults to `qwen3-vl-flash` (an image-recognition-specific default, independent of the chat model). |
|
||||||
| `maxTokens` | number | Generated-token bound; defaults to 2048. |
|
| `maxTokens` | number | Generated-token bound; defaults to 2048. |
|
||||||
|
|
||||||
The endpoint and key are editable live through the `image-recognition-http`
|
The endpoint and key are editable live through the `image-recognition-http`
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {} from '@deepseek-ai/dsh-session'
|
|||||||
import type {} from '@deepseek-ai/dsh-image-recognition'
|
import type {} from '@deepseek-ai/dsh-image-recognition'
|
||||||
import {
|
import {
|
||||||
ImageRecognitionHttpProvider,
|
ImageRecognitionHttpProvider,
|
||||||
|
IMAGE_RECOGNITION_DEFAULT_BASE_URL,
|
||||||
IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
||||||
IMAGE_RECOGNITION_DEFAULT_MODEL,
|
IMAGE_RECOGNITION_DEFAULT_MODEL,
|
||||||
} from './provider.ts'
|
} from './provider.ts'
|
||||||
@@ -23,6 +24,7 @@ import type { ImageRecognitionHttpProviderOptions } from './provider.ts'
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
ImageRecognitionHttpProvider,
|
ImageRecognitionHttpProvider,
|
||||||
|
IMAGE_RECOGNITION_DEFAULT_BASE_URL,
|
||||||
IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
||||||
IMAGE_RECOGNITION_DEFAULT_MODEL,
|
IMAGE_RECOGNITION_DEFAULT_MODEL,
|
||||||
IMAGE_RECOGNITION_HTTP_PROVIDER_ID,
|
IMAGE_RECOGNITION_HTTP_PROVIDER_ID,
|
||||||
@@ -35,17 +37,19 @@ export const name = 'image-recognition-http'
|
|||||||
/** The image-recognition seam this provider registers into. */
|
/** The image-recognition seam this provider registers into. */
|
||||||
export const inject = ['imageRecognition']
|
export const inject = ['imageRecognition']
|
||||||
|
|
||||||
const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY'
|
// Distinct from the main chat model's key: image recognition may use a
|
||||||
|
// different provider (e.g. Aliyun DashScope) than the conversation LLM.
|
||||||
|
const DEFAULT_API_KEY_ENV = 'IMAGE_RECOGNITION_API_KEY'
|
||||||
|
|
||||||
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
|
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/** Literal API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
|
/** Literal API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
/** Credential reference resolved for each recognition; defaults to `DEEPSEEK_API_KEY`. */
|
/** Credential reference resolved per recognition; defaults to `IMAGE_RECOGNITION_API_KEY`. */
|
||||||
apiKeyEnv?: string
|
apiKeyEnv?: string
|
||||||
/** OpenAI-compatible endpoint base; `/chat/completions` is appended. */
|
/** OpenAI-compatible endpoint base; `/chat/completions` is appended. Defaults to Aliyun DashScope compatible-mode. */
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
/** Vision model name. Defaults to `deepseek-v4-flash`. */
|
/** Vision model name; defaults to `qwen3-vl-flash` (an image-recognition-specific default, not the chat model). */
|
||||||
model?: string
|
model?: string
|
||||||
/** Upper bound on generated tokens. Defaults to 2048. */
|
/** Upper bound on generated tokens. Defaults to 2048. */
|
||||||
maxTokens?: number
|
maxTokens?: number
|
||||||
@@ -54,7 +58,7 @@ export interface Config {
|
|||||||
export const Config: z<Config> = z.object({
|
export const Config: z<Config> = z.object({
|
||||||
apiKey: z.string().role('secret'),
|
apiKey: z.string().role('secret'),
|
||||||
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
||||||
baseURL: z.string(),
|
baseURL: z.string().default(IMAGE_RECOGNITION_DEFAULT_BASE_URL),
|
||||||
model: z.string().default(IMAGE_RECOGNITION_DEFAULT_MODEL),
|
model: z.string().default(IMAGE_RECOGNITION_DEFAULT_MODEL),
|
||||||
maxTokens: z.number().step(1).min(1).default(IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS),
|
maxTokens: z.number().step(1).min(1).default(IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS),
|
||||||
})
|
})
|
||||||
@@ -89,7 +93,7 @@ function resolveOptions(ctx: Context, config: Config): ImageRecognitionHttpProvi
|
|||||||
},
|
},
|
||||||
baseURL: config.baseURL
|
baseURL: config.baseURL
|
||||||
?? launchEnvironmentOf(ctx).get(BASE_URL_ENV)?.value
|
?? launchEnvironmentOf(ctx).get(BASE_URL_ENV)?.value
|
||||||
?? '',
|
?? IMAGE_RECOGNITION_DEFAULT_BASE_URL,
|
||||||
model: config.model ?? IMAGE_RECOGNITION_DEFAULT_MODEL,
|
model: config.model ?? IMAGE_RECOGNITION_DEFAULT_MODEL,
|
||||||
maxTokens: config.maxTokens ?? IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
maxTokens: config.maxTokens ?? IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS,
|
||||||
recordRequest: (request) => {
|
recordRequest: (request) => {
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IMAGE_RECOGNITION_DEFAULT_MODEL = 'deepseek-v4-flash'
|
export const IMAGE_RECOGNITION_DEFAULT_MODEL = 'qwen3-vl-flash'
|
||||||
export const IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS = 2048
|
export const IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS = 2048
|
||||||
|
export const IMAGE_RECOGNITION_DEFAULT_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
||||||
|
|
||||||
const USER_AGENT = 'deepseek-harness/0.0.1'
|
const USER_AGENT = 'deepseek-harness/0.0.1'
|
||||||
|
|
||||||
@@ -115,6 +116,12 @@ export class ImageRecognitionHttpProvider implements ImageRecognitionProvider {
|
|||||||
'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING',
|
'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (options.model.length === 0) {
|
||||||
|
throw new ImageRecognitionError(
|
||||||
|
'image-recognition provider has no model configured; set one in the plugin settings',
|
||||||
|
'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING',
|
||||||
|
)
|
||||||
|
}
|
||||||
const source = await imageSource(request.image)
|
const source = await imageSource(request.image)
|
||||||
const payload = {
|
const payload = {
|
||||||
model: options.model,
|
model: options.model,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import type { ImageRecognitionRequest } from '@deepseek-ai/dsh-image-recognition'
|
import type { ImageRecognitionRequest } from '@deepseek-ai/dsh-image-recognition'
|
||||||
import {
|
import {
|
||||||
ImageRecognitionHttpProvider,
|
ImageRecognitionHttpProvider,
|
||||||
IMAGE_RECOGNITION_DEFAULT_MODEL,
|
|
||||||
type ImageRecognitionHttpProviderOptions,
|
type ImageRecognitionHttpProviderOptions,
|
||||||
} from '../src/provider.ts'
|
} from '../src/provider.ts'
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ function makeProvider(overrides: Partial<ImageRecognitionHttpProviderOptions> =
|
|||||||
return new ImageRecognitionHttpProvider(() => ({
|
return new ImageRecognitionHttpProvider(() => ({
|
||||||
resolveApiKey: async () => 'key',
|
resolveApiKey: async () => 'key',
|
||||||
baseURL: 'https://vision.example.com/v1',
|
baseURL: 'https://vision.example.com/v1',
|
||||||
model: IMAGE_RECOGNITION_DEFAULT_MODEL,
|
model: 'vision-model',
|
||||||
maxTokens: 128,
|
maxTokens: 128,
|
||||||
...overrides,
|
...overrides,
|
||||||
}))
|
}))
|
||||||
@@ -60,6 +59,11 @@ describe('ImageRecognitionHttpProvider.recognize', () => {
|
|||||||
.rejects.toThrow(expect.objectContaining({ code: 'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING' }))
|
.rejects.toThrow(expect.objectContaining({ code: 'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING' }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('throws IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING without a model', async () => {
|
||||||
|
await expect(makeProvider({ model: '' }).recognize(request))
|
||||||
|
.rejects.toThrow(expect.objectContaining({ code: 'IMAGE_RECOGNITION_PROVIDER_CREDENTIAL_MISSING' }))
|
||||||
|
})
|
||||||
|
|
||||||
it('maps a non-2xx response to IMAGE_RECOGNITION_PROVIDER_ERROR', async () => {
|
it('maps a non-2xx response to IMAGE_RECOGNITION_PROVIDER_ERROR', async () => {
|
||||||
stubFetch({ ok: false, status: 429, json: async () => ({ error: { message: 'rate limited' } }) })
|
stubFetch({ ok: false, status: 429, json: async () => ({ error: { message: 'rate limited' } }) })
|
||||||
await expect(makeProvider().recognize(request))
|
await expect(makeProvider().recognize(request))
|
||||||
|
|||||||
Reference in New Issue
Block a user