diff --git a/packages/vision/image-recognition/src/index.ts b/packages/vision/image-recognition/src/index.ts index 17c269578d..6c0da6726f 100644 --- a/packages/vision/image-recognition/src/index.ts +++ b/packages/vision/image-recognition/src/index.ts @@ -102,6 +102,26 @@ export class ImageRecognitionRuntime extends Service { return () => void dispose() } + /** + * Whether a provider can currently serve recognition. True when a configured + * provider is registered and usable, or exactly one usable provider is + * registered without a pin. Cheap and side-effect-free (no network): it reads + * only the registry and each provider's local `available()` check, so a + * consumer can gate its surface on configuration without attempting a call. + * @returns whether recognition is currently configured and usable. + */ + available(): boolean { + try { + resolveProvider({ + providers: this.providers, + ...this.providerId !== undefined ? { configuredId: this.providerId } : {}, + }) + return true + } catch { + return false + } + } + /** * Run one recognition through the selected provider. Resolves the provider at * call time with the selection rules above; throws {@link ImageRecognitionError} diff --git a/packages/vision/image-recognition/tests/image-recognition.spec.ts b/packages/vision/image-recognition/tests/image-recognition.spec.ts index a97cb6f5b6..29400d4d95 100644 --- a/packages/vision/image-recognition/tests/image-recognition.spec.ts +++ b/packages/vision/image-recognition/tests/image-recognition.spec.ts @@ -98,3 +98,22 @@ describe('ImageRecognitionRuntime selection', () => { .rejects.toThrow(expect.objectContaining({ code: 'IMAGE_RECOGNITION_PROVIDER_AMBIGUOUS' })) }) }) + +describe('ImageRecognitionRuntime.available', () => { + it('is false with no registered provider', async () => { + const { runtime } = await mountRuntime() + expect(runtime.available()).toBe(false) + }) + + it('is false when the configured provider is unavailable (not configured)', async () => { + const { runtime } = await mountRuntime({ provider: 'http' }) + runtime.registerProvider(makeProvider('http', unavailable, () => Promise.resolve(recognizeResult('a')))) + expect(runtime.available()).toBe(false) + }) + + it('is true when a configured provider is available', async () => { + const { runtime } = await mountRuntime({ provider: 'http' }) + runtime.registerProvider(makeProvider('http', available, () => Promise.resolve(recognizeResult('a')))) + expect(runtime.available()).toBe(true) + }) +}) diff --git a/packages/vision/tool-image-recognition/README.md b/packages/vision/tool-image-recognition/README.md index 6c8c1addec..5d2e935754 100644 --- a/packages/vision/tool-image-recognition/README.md +++ b/packages/vision/tool-image-recognition/README.md @@ -96,5 +96,9 @@ Prefix-stable while the guidance text is unchanged. ## Known Limitations and Deferred Work +- **Dormant until configured:** the image-task injection is gated on a usable + provider, so without a configured endpoint the capability is inert and the + model is not guided to recognition. Direct `recognize_image` calls then throw + a clear `ImageRecognitionError`. - Path/URL detection is heuristic and can be disabled; misdetections are possible. - Requires a configured, available provider — see `dsh-image-recognition-http`. diff --git a/packages/vision/tool-image-recognition/src/index.ts b/packages/vision/tool-image-recognition/src/index.ts index 6e6c5eef46..2582180a8a 100644 --- a/packages/vision/tool-image-recognition/src/index.ts +++ b/packages/vision/tool-image-recognition/src/index.ts @@ -150,6 +150,9 @@ export function apply(ctx: Context, config: Config = {}): void { const decision = await next() if (decision.kind === 'reject') return decision if (!hasImageSignal(decision.messages, detectBlocks, detectPaths)) return decision + // Dormant until configured: without a usable provider the recognition + // surface is inert, so the model is not guided to a call that can only fail. + if (!ctx.imageRecognition.available()) return decision signal.throwIfAborted() const lookup = { cwd: agent.session.header.cwd, signal, scope: agent } const skill = await ctx.skills.get(SKILL_NAME, lookup)