mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
feat(acp): advertise vision-bridge image capability in initialize response (#6269)
* feat(acp): advertise vision-bridge image capability in initialize response Adds `_meta.imageCapability` to both stdio and HTTP ACP initialize responses so external hosts like sudowork can feature-detect native image handling instead of maintaining a hardcoded allowlist. Resolves #6086 * test(acp): cover image capability advertisement * docs(acp): clarify image capability threshold * fix(acp): align image capability contract
This commit is contained in:
parent
bd9bf50aa8
commit
aa4bccfc47
9 changed files with 80 additions and 11 deletions
|
|
@ -148,6 +148,11 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
|
|||
AGENT: 'agent',
|
||||
},
|
||||
FORK_SUBAGENT_TYPE: 'fork',
|
||||
IMAGE_CAPABILITY: Object.freeze({
|
||||
autoHandlesWrongModel: true,
|
||||
maxBytes: 10380902,
|
||||
maxImagesPerTurn: 4,
|
||||
}),
|
||||
ALL_PROVIDERS: [
|
||||
{
|
||||
id: 'deepseek',
|
||||
|
|
@ -1298,6 +1303,13 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
|
|||
sse: true,
|
||||
http: true,
|
||||
},
|
||||
_meta: {
|
||||
imageCapability: {
|
||||
autoHandlesWrongModel: true,
|
||||
maxBytes: 10380902,
|
||||
maxImagesPerTurn: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import {
|
|||
runManagedAutoMemoryDream,
|
||||
runManagedRememberByAgent,
|
||||
matchesAnyServerPattern,
|
||||
IMAGE_CAPABILITY,
|
||||
registerAcpEventLoopLagGauge,
|
||||
startEventLoopLagMonitor,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -2832,6 +2833,9 @@ class QwenAgent implements Agent {
|
|||
sse: true,
|
||||
http: true,
|
||||
},
|
||||
_meta: {
|
||||
imageCapability: IMAGE_CAPABILITY,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
writeWorkspaceContextFile,
|
||||
type SessionArchiveState,
|
||||
type SubagentLevel,
|
||||
IMAGE_CAPABILITY,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
// Import the permission error classes from the same module REST's
|
||||
// `sendPermissionVoteError` uses, so `instanceof` matches the class the bridge
|
||||
|
|
@ -843,6 +844,7 @@ export class AcpDispatcher {
|
|||
this.sessionShellCommandEnabled,
|
||||
),
|
||||
},
|
||||
imageCapability: IMAGE_CAPABILITY,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -979,6 +979,20 @@ describe('ACP Streamable HTTP transport (over the wire)', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('initialize advertises image capability at _meta.imageCapability', async () => {
|
||||
const { body } = await initializeRaw();
|
||||
const result = body['result'] as {
|
||||
agentCapabilities: {
|
||||
_meta: { imageCapability: Record<string, unknown> };
|
||||
};
|
||||
};
|
||||
expect(result.agentCapabilities._meta.imageCapability).toEqual({
|
||||
autoHandlesWrongModel: true,
|
||||
maxBytes: 10380902,
|
||||
maxImagesPerTurn: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('initialize advertises _qwen/workspace/permissions methods', async () => {
|
||||
const { body } = await initializeRaw();
|
||||
const result = body['result'] as {
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ export * from './services/gitWorktreeService.js';
|
|||
export { DEFAULT_MAX_TOOL_CALLS_PER_TURN } from './services/loopDetectionService.js';
|
||||
export * from './services/visionBridge/vision-bridge-service.js';
|
||||
export * from './services/visionBridge/image-part-utils.js';
|
||||
export * from './services/visionBridge/image-capability.js';
|
||||
export * from './services/sessionRecap.js';
|
||||
export * from './services/sessionService.js';
|
||||
export * from './services/sessionTitle.js';
|
||||
|
|
|
|||
28
packages/core/src/services/visionBridge/image-capability.ts
Normal file
28
packages/core/src/services/visionBridge/image-capability.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Vision-bridge capability advertisement for ACP initialize responses.
|
||||
*
|
||||
* Boolean flags describe backend-handled behavior for ACP hosts; numeric
|
||||
* thresholds track the same constants enforced by the vision bridge.
|
||||
*
|
||||
* Placed in `agentCapabilities._meta.imageCapability` of the ACP
|
||||
* `initialize` response on both stdio and HTTP paths.
|
||||
*/
|
||||
import {
|
||||
VISION_BRIDGE_MAX_IMAGE_BASE64_BYTES,
|
||||
VISION_BRIDGE_MAX_IMAGES,
|
||||
} from './vision-bridge-constants.js';
|
||||
|
||||
export const IMAGE_CAPABILITY = Object.freeze({
|
||||
/** Text-only active models are handled by the vision bridge when configured. */
|
||||
autoHandlesWrongModel: true,
|
||||
/** Current per-image inline base64 payload cap, in bytes. */
|
||||
maxBytes: VISION_BRIDGE_MAX_IMAGE_BASE64_BYTES,
|
||||
/** Current max images processed per turn. */
|
||||
maxImagesPerTurn: VISION_BRIDGE_MAX_IMAGES,
|
||||
});
|
||||
|
|
@ -5,15 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { Part, PartListUnion } from '@google/genai';
|
||||
|
||||
/**
|
||||
* Conservative cap on a single image part's base64 length (in MB) before the
|
||||
* vision bridge refuses it, so the bridge never makes a side call a provider
|
||||
* would reject for size. Measured on the base64 string, which overstates the
|
||||
* decoded bytes by ~33%, keeping this comfortably under the repo's ~10MB
|
||||
* decoded inline-media ceiling.
|
||||
*/
|
||||
const MAX_IMAGE_BASE64_MB = 9.9;
|
||||
import { VISION_BRIDGE_MAX_IMAGE_BASE64_BYTES } from './vision-bridge-constants.js';
|
||||
|
||||
/**
|
||||
* Normalize a {@link PartListUnion} into a flat array of {@link Part} objects.
|
||||
|
|
@ -159,7 +151,7 @@ export function isUsableImagePart(part: Part): boolean {
|
|||
if (typeof data !== 'string' || data.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return data.length / (1024 * 1024) <= MAX_IMAGE_BASE64_MB;
|
||||
return data.length <= VISION_BRIDGE_MAX_IMAGE_BASE64_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const VISION_BRIDGE_MAX_IMAGES = 4;
|
||||
|
||||
/**
|
||||
* Conservative cap on a single image part's base64 payload before the vision
|
||||
* bridge refuses it. Measured on the inline base64 string, which is what ACP
|
||||
* transports over the wire.
|
||||
*/
|
||||
export const VISION_BRIDGE_MAX_IMAGE_BASE64_BYTES = Math.floor(
|
||||
9.9 * 1024 * 1024,
|
||||
);
|
||||
|
|
@ -16,10 +16,10 @@ import {
|
|||
replaceImagesWithText,
|
||||
splitImageParts,
|
||||
} from './image-part-utils.js';
|
||||
import { VISION_BRIDGE_MAX_IMAGES } from './vision-bridge-constants.js';
|
||||
|
||||
const debugLogger = createDebugLogger('VISION_BRIDGE');
|
||||
const BRIDGE_MAX_OUTPUT_TOKENS = 2048;
|
||||
const VISION_BRIDGE_MAX_IMAGES = 4;
|
||||
const VISION_BRIDGE_TIMEOUT_MS = 30_000;
|
||||
// Cap intent so @-file contents in nonImageParts aren't dumped to the bridge model.
|
||||
const BRIDGE_INTENT_MAX_CHARS = 2000;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue