diff --git a/packages/vision/image-recognition-http/README.md b/packages/vision/image-recognition-http/README.md index c1dd7866d3..8d12035ddf 100644 --- a/packages/vision/image-recognition-http/README.md +++ b/packages/vision/image-recognition-http/README.md @@ -8,10 +8,10 @@ URL and returns the recognized text. | 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`. | -| `apiKeyEnv` | string (credential-ref) | Credential reference resolved per recognition; defaults to `DEEPSEEK_API_KEY`. | -| `model` | string | Vision model name; defaults to `deepseek-v4-flash`. | +| `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 `qwen3-vl-flash` (an image-recognition-specific default, independent of the chat model). | | `maxTokens` | number | Generated-token bound; defaults to 2048. | The endpoint and key are editable live through the `image-recognition-http` diff --git a/packages/vision/image-recognition-http/src/index.ts b/packages/vision/image-recognition-http/src/index.ts index f1ba2b6c80..e9b0291c4a 100644 --- a/packages/vision/image-recognition-http/src/index.ts +++ b/packages/vision/image-recognition-http/src/index.ts @@ -16,6 +16,7 @@ import type {} from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-image-recognition' import { ImageRecognitionHttpProvider, + IMAGE_RECOGNITION_DEFAULT_BASE_URL, IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS, IMAGE_RECOGNITION_DEFAULT_MODEL, } from './provider.ts' @@ -23,6 +24,7 @@ import type { ImageRecognitionHttpProviderOptions } from './provider.ts' export { ImageRecognitionHttpProvider, + IMAGE_RECOGNITION_DEFAULT_BASE_URL, IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS, IMAGE_RECOGNITION_DEFAULT_MODEL, IMAGE_RECOGNITION_HTTP_PROVIDER_ID, @@ -35,17 +37,19 @@ export const name = 'image-recognition-http' /** The image-recognition seam this provider registers into. */ 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). */ export interface Config { /** Literal API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */ 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 - /** OpenAI-compatible endpoint base; `/chat/completions` is appended. */ + /** OpenAI-compatible endpoint base; `/chat/completions` is appended. Defaults to Aliyun DashScope compatible-mode. */ 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 /** Upper bound on generated tokens. Defaults to 2048. */ maxTokens?: number @@ -54,7 +58,7 @@ export interface Config { export const Config: z = z.object({ apiKey: z.string().role('secret'), 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), 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 ?? launchEnvironmentOf(ctx).get(BASE_URL_ENV)?.value - ?? '', + ?? IMAGE_RECOGNITION_DEFAULT_BASE_URL, model: config.model ?? IMAGE_RECOGNITION_DEFAULT_MODEL, maxTokens: config.maxTokens ?? IMAGE_RECOGNITION_DEFAULT_MAX_TOKENS, recordRequest: (request) => { diff --git a/packages/vision/image-recognition-http/src/provider.ts b/packages/vision/image-recognition-http/src/provider.ts index 799cc13f9c..21dd6a6e5a 100644 --- a/packages/vision/image-recognition-http/src/provider.ts +++ b/packages/vision/image-recognition-http/src/provider.ts @@ -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_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1' const USER_AGENT = 'deepseek-harness/0.0.1' @@ -115,6 +116,12 @@ export class ImageRecognitionHttpProvider implements ImageRecognitionProvider { '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 payload = { model: options.model, diff --git a/packages/vision/image-recognition-http/tests/provider.spec.ts b/packages/vision/image-recognition-http/tests/provider.spec.ts index aefbd54783..725269db56 100644 --- a/packages/vision/image-recognition-http/tests/provider.spec.ts +++ b/packages/vision/image-recognition-http/tests/provider.spec.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { ImageRecognitionRequest } from '@deepseek-ai/dsh-image-recognition' import { ImageRecognitionHttpProvider, - IMAGE_RECOGNITION_DEFAULT_MODEL, type ImageRecognitionHttpProviderOptions, } from '../src/provider.ts' @@ -15,7 +14,7 @@ function makeProvider(overrides: Partial = return new ImageRecognitionHttpProvider(() => ({ resolveApiKey: async () => 'key', baseURL: 'https://vision.example.com/v1', - model: IMAGE_RECOGNITION_DEFAULT_MODEL, + model: 'vision-model', maxTokens: 128, ...overrides, })) @@ -60,6 +59,11 @@ describe('ImageRecognitionHttpProvider.recognize', () => { .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 () => { stubFetch({ ok: false, status: 429, json: async () => ({ error: { message: 'rate limited' } }) }) await expect(makeProvider().recognize(request))