feat(vision): gate recognition on a configured provider
Expose ImageRecognitionRuntime.available() and gate the image-task pre-step injection on it, so the capability is dormant until an endpoint is configured instead of guiding the model to a call that can only fail. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,26 @@ export class ImageRecognitionRuntime extends Service {
|
|||||||
return () => void dispose()
|
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
|
* Run one recognition through the selected provider. Resolves the provider at
|
||||||
* call time with the selection rules above; throws {@link ImageRecognitionError}
|
* call time with the selection rules above; throws {@link ImageRecognitionError}
|
||||||
|
|||||||
@@ -98,3 +98,22 @@ describe('ImageRecognitionRuntime selection', () => {
|
|||||||
.rejects.toThrow(expect.objectContaining({ code: 'IMAGE_RECOGNITION_PROVIDER_AMBIGUOUS' }))
|
.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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -96,5 +96,9 @@ Prefix-stable while the guidance text is unchanged.
|
|||||||
|
|
||||||
## Known Limitations and Deferred Work
|
## 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.
|
- Path/URL detection is heuristic and can be disabled; misdetections are possible.
|
||||||
- Requires a configured, available provider — see `dsh-image-recognition-http`.
|
- Requires a configured, available provider — see `dsh-image-recognition-http`.
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|||||||
const decision = await next()
|
const decision = await next()
|
||||||
if (decision.kind === 'reject') return decision
|
if (decision.kind === 'reject') return decision
|
||||||
if (!hasImageSignal(decision.messages, detectBlocks, detectPaths)) 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()
|
signal.throwIfAborted()
|
||||||
const lookup = { cwd: agent.session.header.cwd, signal, scope: agent }
|
const lookup = { cwd: agent.session.header.cwd, signal, scope: agent }
|
||||||
const skill = await ctx.skills.get(SKILL_NAME, lookup)
|
const skill = await ctx.skills.get(SKILL_NAME, lookup)
|
||||||
|
|||||||
Reference in New Issue
Block a user