1338 lines
40 KiB
Markdown
1338 lines
40 KiB
Markdown
# Phase 4 - OpenCode file parts and model vision capability gate
|
||
⚠️ **Историческая документация**: Этот файл содержит дизайн-документацию. Фактическая реализация в `src/features/agent-attachments/` может отличаться от описанной архитектуры.
|
||
|
||
## Summary
|
||
|
||
Goal: support image attachments for OpenCode teammates only when the selected model/runtime can actually accept image parts, and show clear UI errors for text-only models.
|
||
|
||
Chosen approach: **OpenCode file-part adapter + curated model vision capability catalog + unknown capability fail-safe + optional live smoke tooling**.
|
||
|
||
🎯 8.3 🛡️ 8.0 🧠 7.2
|
||
Estimated change size: `320-560` LOC across two repos.
|
||
|
||
Repos:
|
||
|
||
- `/Users/belief/dev/projects/claude/claude_team`
|
||
- `/Users/belief/dev/projects/claude/agent_teams_orchestrator`
|
||
|
||
## Live proof
|
||
|
||
Validated manually:
|
||
|
||
```text
|
||
opencode run --model openai/gpt-5.4-mini -f red-card-valid.png -> red
|
||
opencode run --model openrouter/moonshotai/kimi-k2.6 -f red-card-valid.png -> red
|
||
opencode run --model openrouter/z-ai/glm-4.5v -f red-card-valid.png -> red
|
||
opencode run --model openrouter/z-ai/glm-5.1 -f red-card-valid.png -> model says it cannot view images
|
||
```
|
||
|
||
OpenCode session export shows file part shape:
|
||
|
||
```json
|
||
{
|
||
"type": "file",
|
||
"mime": "image/png",
|
||
"url": "data:image/png;base64,...",
|
||
"filename": "red-card-valid.png"
|
||
}
|
||
```
|
||
|
||
Therefore transport can carry images, but model capability must gate usage.
|
||
|
||
## Current behavior
|
||
|
||
Current desktop delivery blocks OpenCode secondary attachments:
|
||
|
||
```text
|
||
opencode_attachments_not_supported_for_secondary_runtime
|
||
```
|
||
|
||
This was safe. Phase 4 replaces the blanket block with a capability-aware path.
|
||
|
||
## Capability catalog
|
||
|
||
Create a pure catalog in the attachment feature or shared OpenCode model utilities.
|
||
|
||
```ts
|
||
export type VisionCapability =
|
||
| { kind: 'supported'; source: 'curated' | 'provider-metadata' | 'live-probe' }
|
||
| { kind: 'unsupported'; source: 'curated' | 'model-response' | 'provider-metadata'; reason: string }
|
||
| { kind: 'unknown'; reason: string };
|
||
|
||
export function resolveOpenCodeVisionCapability(modelId: string): VisionCapability {
|
||
const normalized = normalizeOpenCodeModelId(modelId);
|
||
|
||
if (normalized === 'openrouter/z-ai/glm-5.1') {
|
||
return {
|
||
kind: 'unsupported',
|
||
source: 'curated',
|
||
reason: 'GLM 5.1 did not accept image input in live OpenCode/OpenRouter smoke test.',
|
||
};
|
||
}
|
||
|
||
if (
|
||
normalized === 'openai/gpt-5.4-mini' ||
|
||
normalized === 'openrouter/moonshotai/kimi-k2.6' ||
|
||
normalized === 'openrouter/z-ai/glm-4.5v'
|
||
) {
|
||
return { kind: 'supported', source: 'curated' };
|
||
}
|
||
|
||
if (/\b(vl|vision|image)\b/i.test(normalized)) {
|
||
return { kind: 'supported', source: 'provider-metadata' };
|
||
}
|
||
|
||
return {
|
||
kind: 'unknown',
|
||
reason: 'Image capability for this OpenCode model has not been verified.',
|
||
};
|
||
}
|
||
```
|
||
|
||
Policy decision:
|
||
|
||
- `supported`: allow image delivery.
|
||
- `unsupported`: block with clear error.
|
||
- `unknown`: block by default for production sends, but allow future manual override only if explicitly designed.
|
||
|
||
Before release, do not allow unknown models to receive images silently.
|
||
|
||
## OpenCode adapter
|
||
|
||
```ts
|
||
export class OpenCodeAttachmentAdapter implements AttachmentDeliveryAdapter {
|
||
readonly runtimeKind = 'opencode' as const;
|
||
|
||
canDeliver(ctx: AttachmentRuntimeContext, attachment: NormalizedAgentAttachment) {
|
||
if (attachment.kind !== 'image') {
|
||
return block('OpenCode currently supports image attachments only.');
|
||
}
|
||
|
||
const capability = resolveOpenCodeVisionCapability(ctx.modelId);
|
||
if (capability.kind === 'unsupported') {
|
||
return block(`${ctx.modelId} does not support image input. ${capability.reason}`);
|
||
}
|
||
if (capability.kind === 'unknown') {
|
||
return block(`Image input support for ${ctx.modelId} is unknown. Choose a verified vision model.`);
|
||
}
|
||
|
||
return allowIfMime(attachment, ['image/png', 'image/jpeg', 'image/webp']);
|
||
}
|
||
|
||
async prepare(ctx: AttachmentRuntimeContext, attachment: NormalizedAgentAttachment) {
|
||
const variant = selectOpenCodeFilePartVariant(attachment);
|
||
return {
|
||
runtimeKind: 'opencode',
|
||
attachmentId: attachment.id,
|
||
part: {
|
||
kind: 'opencode-file-part',
|
||
value: {
|
||
type: 'file',
|
||
mime: variant.mimeType,
|
||
url: toDataUrl(variant),
|
||
filename: attachment.originalName,
|
||
},
|
||
},
|
||
diagnostics: [`prepared OpenCode file part for ${ctx.modelId}`],
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
## Orchestrator OpenCode bridge changes
|
||
|
||
Current bridge sends text-only parts. Extend to accept file parts.
|
||
|
||
```ts
|
||
export interface OpenCodePromptPart {
|
||
type: 'text' | 'file';
|
||
text?: string;
|
||
mime?: string;
|
||
url?: string;
|
||
filename?: string;
|
||
}
|
||
|
||
export interface SendOpenCodePromptInput {
|
||
sessionId: string;
|
||
parts: OpenCodePromptPart[];
|
||
}
|
||
```
|
||
|
||
When no attachments:
|
||
|
||
```ts
|
||
parts: [{ type: 'text', text: params.text }]
|
||
```
|
||
|
||
When attachments:
|
||
|
||
```ts
|
||
parts: [
|
||
{ type: 'text', text: params.text },
|
||
{ type: 'file', mime: 'image/png', url: 'data:image/png;base64,...', filename: 'screenshot.png' },
|
||
]
|
||
```
|
||
|
||
Do not change proof gates. OpenCode delivery success still requires existing response proof:
|
||
|
||
- visible reply;
|
||
- safe plain-text materialization;
|
||
- `message_send` with correct relay id;
|
||
- work-sync report;
|
||
- task/progress evidence.
|
||
|
||
Attachment accepted by OpenCode is not response proof.
|
||
|
||
## UI capability behavior
|
||
|
||
Examples:
|
||
|
||
```text
|
||
GLM 5.1 does not support image input. Choose GLM 4.5V, Kimi K2.6, GPT-5.4-mini, Claude, or Codex.
|
||
```
|
||
|
||
```text
|
||
Image input support for this OpenCode model is not verified. Choose a verified vision model before sending screenshots.
|
||
```
|
||
|
||
```text
|
||
This image will be sent to Kimi K2.6 through OpenCode/OpenRouter.
|
||
```
|
||
|
||
Keep this next to attachment previews and also validate at send time.
|
||
|
||
## Edge cases
|
||
|
||
### OpenRouter provider missing
|
||
|
||
If OpenCode has no OpenRouter key/provider, model list may not include `openrouter/*`. This is provider auth/config issue, not attachment issue.
|
||
|
||
Expected error:
|
||
|
||
```text
|
||
OpenRouter is not connected in OpenCode. Connect OpenRouter before using this model.
|
||
```
|
||
|
||
### OpenRouter credits exhausted
|
||
|
||
Preserve exact provider error. Do not rewrite it as attachment failure.
|
||
|
||
### Model accepts file part but says cannot see image
|
||
|
||
Treat this as model capability failure and update curated deny list only after repeated confirmed cases.
|
||
|
||
Do not mark delivery transport failed if OpenCode accepted the prompt and model responded.
|
||
|
||
### Unknown model
|
||
|
||
Block image attachments by default. Text-only messages still work.
|
||
|
||
### Multiple images
|
||
|
||
Allow only if total optimized budget is safe. Do not send partial images.
|
||
|
||
### File part size
|
||
|
||
Use same optimized variants as Claude/Codex. OpenCode data URL still has base64 overhead, so backend serialized budget applies.
|
||
|
||
### Retry
|
||
|
||
Retry must reuse the same original/variant metadata. Do not recompress differently on every retry unless original variant is missing.
|
||
|
||
## Tests
|
||
|
||
### Unit
|
||
|
||
- `openrouter/moonshotai/kimi-k2.6` is supported;
|
||
- `openrouter/z-ai/glm-4.5v` is supported;
|
||
- `openrouter/z-ai/glm-5.1` is unsupported;
|
||
- unknown OpenRouter model is blocked for image;
|
||
- OpenCode adapter emits `file` part with data URL;
|
||
- no base64 appears in diagnostics;
|
||
- text-only OpenCode messages remain unchanged.
|
||
|
||
### Bridge tests
|
||
|
||
- OpenCode bridge accepts text-only parts;
|
||
- OpenCode bridge accepts text plus file parts;
|
||
- unsupported part type rejects before API call;
|
||
- response observer/proof semantics unchanged.
|
||
|
||
### Desktop service tests
|
||
|
||
- direct OpenCode secondary image send blocks unsupported model;
|
||
- direct OpenCode secondary image send prepares file part for supported model;
|
||
- provider/API error is preserved;
|
||
- attachment accepted does not set delivery success without response proof.
|
||
|
||
### Live e2e
|
||
|
||
Only on explicit request:
|
||
|
||
```bash
|
||
OPENROUTER_API_KEY=... opencode run --pure --format json --dir /tmp --model openrouter/moonshotai/kimi-k2.6 "..." -f red-card-valid.png
|
||
OPENROUTER_API_KEY=... opencode run --pure --format json --dir /tmp --model openrouter/z-ai/glm-4.5v "..." -f red-card-valid.png
|
||
OPENROUTER_API_KEY=... opencode run --pure --format json --dir /tmp --model openrouter/z-ai/glm-5.1 "..." -f red-card-valid.png
|
||
```
|
||
|
||
Expected:
|
||
|
||
```text
|
||
Kimi K2.6 -> red
|
||
GLM 4.5V -> red
|
||
GLM 5.1 -> unsupported/refusal
|
||
```
|
||
|
||
## Safety checklist
|
||
|
||
- OpenCode proof gates unchanged.
|
||
- Unsupported models blocked before send.
|
||
- Provider auth errors preserved.
|
||
- No silent fallback to text base64.
|
||
- No model-specific prompt hacks.
|
||
- Capability catalog is pure and unit-tested.
|
||
|
||
## Deep implementation details
|
||
|
||
### Capability resolver should be pure
|
||
|
||
Do not call OpenCode or OpenRouter inside the send hot path.
|
||
|
||
```ts
|
||
export interface OpenCodeVisionCapabilityResolver {
|
||
resolve(modelId: string): VisionCapability;
|
||
}
|
||
```
|
||
|
||
Use static curated rules plus model id heuristics. Live probing belongs in Phase 5 tooling, not every send.
|
||
|
||
### Capability result examples
|
||
|
||
```ts
|
||
resolve('openrouter/moonshotai/kimi-k2.6')
|
||
// { kind: 'supported', source: 'curated' }
|
||
|
||
resolve('openrouter/z-ai/glm-4.5v')
|
||
// { kind: 'supported', source: 'curated' }
|
||
|
||
resolve('openrouter/z-ai/glm-5.1')
|
||
// { kind: 'unsupported', source: 'curated', reason: 'Live smoke returned text-only refusal.' }
|
||
|
||
resolve('openrouter/qwen/qwen2.5-vl-72b-instruct')
|
||
// { kind: 'supported', source: 'provider-metadata' }
|
||
|
||
resolve('openrouter/minimax/minimax-m2.5')
|
||
// { kind: 'unknown', reason: 'No verified vision capability.' }
|
||
```
|
||
|
||
### Model normalization
|
||
|
||
OpenCode may expose ids in different forms:
|
||
|
||
```text
|
||
openrouter/moonshotai/kimi-k2.6
|
||
moonshotai/kimi-k2.6 via openrouter provider context
|
||
z-ai/glm-4.5v
|
||
```
|
||
|
||
Normalize consistently:
|
||
|
||
```ts
|
||
export function normalizeOpenCodeModelRef(input: string, providerId?: string): string {
|
||
const trimmed = input.trim().toLowerCase();
|
||
if (trimmed.startsWith('openrouter/')) return trimmed;
|
||
if (providerId === 'openrouter') return `openrouter/${trimmed}`;
|
||
return trimmed;
|
||
}
|
||
```
|
||
|
||
### OpenCode bridge part schema
|
||
|
||
Use narrow supported schema. Do not allow arbitrary JSON parts from renderer/main.
|
||
|
||
```ts
|
||
export type OpenCodeBridgePromptPart =
|
||
| { type: 'text'; text: string }
|
||
| { type: 'file'; mime: 'image/png' | 'image/jpeg' | 'image/webp'; url: string; filename: string };
|
||
|
||
function assertOpenCodeBridgePromptPart(part: OpenCodeBridgePromptPart): void {
|
||
if (part.type === 'file') {
|
||
if (!part.url.startsWith(`data:${part.mime};base64,`)) {
|
||
throw new Error('Invalid OpenCode file part data URL.');
|
||
}
|
||
if (part.url.length > OPENCODE_FILE_PART_DATA_URL_MAX_CHARS) {
|
||
throw new Error('OpenCode file part is too large.');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Delivery result semantics
|
||
|
||
Do not change existing OpenCode delivery proof. Attachment support is input preparation only.
|
||
|
||
```text
|
||
file part accepted != response delivered
|
||
response text exists != relay proof for peer messages
|
||
empty assistant turn != success
|
||
provider auth failure != retryable prompt repair
|
||
```
|
||
|
||
### UI capability copy
|
||
|
||
Keep model-specific text concise:
|
||
|
||
```text
|
||
Images are not supported by GLM 5.1 in OpenCode. Choose GLM 4.5V, Kimi K2.6, GPT-5.4-mini, Claude, or Codex.
|
||
```
|
||
|
||
For unknown:
|
||
|
||
```text
|
||
Image support for this OpenCode model is not verified. Send text only or choose a verified vision model.
|
||
```
|
||
|
||
For missing provider auth:
|
||
|
||
```text
|
||
OpenRouter is not connected for OpenCode. Connect OpenRouter before sending images to this model.
|
||
```
|
||
|
||
### More edge cases
|
||
|
||
| Edge case | Expected behavior |
|
||
|---|---|
|
||
| OpenCode provider exists only through env key during tests | live smoke works, app-managed config still needs explicit provider setup |
|
||
| User pastes image to OpenCode text-only model | composer blocks send before creating ledger record |
|
||
| User sends text-only to GLM 5.1 | allowed, no image capability warning blocking send |
|
||
| Kimi accepts image but returns low-quality answer | delivery success if response proof exists; not attachment bug |
|
||
| OpenRouter key has quota limit | preserve exact provider error and show advisory/notification if runtime error path triggers |
|
||
| OpenCode returns `ProviderModelNotFoundError` | provider/model config error, not attachment serialization |
|
||
| File part accepted but no assistant response | existing OpenCode repair policy handles no response, not attachment feature |
|
||
| Unsupported image MIME like SVG | block before OpenCode bridge |
|
||
| Data URL too large | block before OpenCode API call |
|
||
|
||
### Negative test for GLM 5.1
|
||
|
||
```ts
|
||
test('blocks GLM 5.1 image send before OpenCode prompt', async () => {
|
||
const result = planner.canPrepare({ runtimeKind: 'opencode', modelId: 'openrouter/z-ai/glm-5.1' }, [image]);
|
||
expect(result.allowed).toBe(false);
|
||
expect(result.blockers[0].code).toBe('attachment_model_vision_unsupported');
|
||
});
|
||
```
|
||
|
||
### Positive test for Kimi
|
||
|
||
```ts
|
||
test('allows Kimi K2.6 image send', async () => {
|
||
const result = planner.canPrepare({ runtimeKind: 'opencode', modelId: 'openrouter/moonshotai/kimi-k2.6' }, [image]);
|
||
expect(result.allowed).toBe(true);
|
||
});
|
||
```
|
||
|
||
### Regression traps
|
||
|
||
- Making unknown OpenCode models permissive by default.
|
||
- Marking ledger `delivered` just because OpenCode accepted a file part.
|
||
- Adding model-specific prompt text like “you can see image” instead of capability gating.
|
||
- Writing OpenRouter API key into logs while running live smoke.
|
||
- Updating app-managed OpenCode auth store from ephemeral env without explicit user action.
|
||
|
||
## File-by-file implementation plan
|
||
|
||
### claude_team
|
||
|
||
Potential files:
|
||
|
||
```text
|
||
src/features/agent-attachments/core/domain/OpenCodeVisionCapability.ts
|
||
src/features/agent-attachments/main/adapters/output/OpenCodeAttachmentAdapter.ts
|
||
src/main/services/team/TeamProvisioningService.ts
|
||
src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts
|
||
src/renderer/components/team/messages/MessageComposer.tsx
|
||
```
|
||
|
||
Do not place the capability map inside `TeamProvisioningService`.
|
||
|
||
### agent_teams_orchestrator
|
||
|
||
Potential files:
|
||
|
||
```text
|
||
src/services/opencode/OpenCodeSessionBridge.ts
|
||
src/services/opencode/OpenCodeBridgeCommandHandler.ts
|
||
src/services/opencode/OpenCodeDeliveryResponseObserver.ts
|
||
src/services/opencode/*.test.ts
|
||
```
|
||
|
||
Bridge accepts structured parts. Observer/proof logic should remain unchanged except diagnostics may include attachment context.
|
||
|
||
## Capability map structure
|
||
|
||
Use a compact data file or pure module.
|
||
|
||
```ts
|
||
const OPENCODE_VISION_CAPABILITY_OVERRIDES: Record<string, VisionCapability> = {
|
||
'openai/gpt-5.4-mini': { kind: 'supported', source: 'curated' },
|
||
'openrouter/moonshotai/kimi-k2.6': { kind: 'supported', source: 'curated' },
|
||
'openrouter/z-ai/glm-4.5v': { kind: 'supported', source: 'curated' },
|
||
'openrouter/z-ai/glm-5.1': {
|
||
kind: 'unsupported',
|
||
source: 'curated',
|
||
reason: 'Live smoke returned model response saying image input is unsupported.',
|
||
},
|
||
};
|
||
```
|
||
|
||
Heuristics only after exact overrides:
|
||
|
||
```ts
|
||
if (/\b(vl|vision|image)\b/i.test(modelId)) {
|
||
return { kind: 'supported', source: 'provider-metadata' };
|
||
}
|
||
```
|
||
|
||
If no exact/heuristic match:
|
||
|
||
```ts
|
||
return { kind: 'unknown', reason: 'No verified vision capability for this model.' };
|
||
```
|
||
|
||
## UI state transitions
|
||
|
||
| State | UI |
|
||
|---|---|
|
||
| supported | normal send enabled |
|
||
| unsupported | send disabled with model-specific message |
|
||
| unknown | send disabled with `not verified` message |
|
||
| provider missing | send disabled with provider connect message |
|
||
| provider quota/auth error after send | runtime error notification/advisory, not preflight warning |
|
||
|
||
## Direct teammate delivery with attachments
|
||
|
||
For OpenCode secondary direct user message:
|
||
|
||
```text
|
||
user -> OpenCode member
|
||
```
|
||
|
||
The send path must:
|
||
|
||
1. resolve member provider/model from live config/meta;
|
||
2. run attachment capability gate;
|
||
3. prepare OpenCode file parts;
|
||
4. call bridge with text + file parts;
|
||
5. keep existing ledger proof semantics.
|
||
|
||
Do not let UI-provided model labels drive capability. Backend must resolve actual member model.
|
||
|
||
## Peer/lead relay with attachments
|
||
|
||
For v1, be conservative.
|
||
|
||
If user sends attachment to lead and lead delegates to OpenCode member, do not automatically forward original binary attachment unless a later phase explicitly implements attachment relay. The lead can describe or ask user to send directly.
|
||
|
||
This avoids accidental broad binary propagation across agents.
|
||
|
||
## More detailed tests
|
||
|
||
### Capability resolver
|
||
|
||
```ts
|
||
it.each([
|
||
['openrouter/moonshotai/kimi-k2.6', 'supported'],
|
||
['openrouter/z-ai/glm-4.5v', 'supported'],
|
||
['openrouter/z-ai/glm-5.1', 'unsupported'],
|
||
['openrouter/minimax/minimax-m2.5', 'unknown'],
|
||
])('resolves %s', (modelId, expected) => {
|
||
expect(resolveOpenCodeVisionCapability(modelId).kind).toBe(expected);
|
||
});
|
||
```
|
||
|
||
### Bridge part validation
|
||
|
||
```ts
|
||
expect(() => assertOpenCodeBridgePromptPart({
|
||
type: 'file',
|
||
mime: 'image/png',
|
||
url: 'not-a-data-url',
|
||
filename: 'x.png',
|
||
})).toThrow('Invalid OpenCode file part data URL');
|
||
```
|
||
|
||
### Ledger preservation
|
||
|
||
```ts
|
||
it('does not mark delivery successful only because file part was accepted', async () => {
|
||
const result = await delivery.sendWithAttachment(...);
|
||
expect(result.delivered).toBe(false);
|
||
expect(result.responsePending).toBe(true);
|
||
});
|
||
```
|
||
|
||
## Review checklist
|
||
|
||
- Unknown OpenCode model does not receive image by default.
|
||
- GLM 5.1 image send is blocked before API call.
|
||
- Kimi K2.6 and GLM 4.5V are allowed.
|
||
- Text-only sends to all models still work.
|
||
- OpenCode provider errors are preserved exactly.
|
||
- Ledger/proof semantics unchanged.
|
||
- No OpenRouter key printed in live test logs.
|
||
|
||
## Phase 4 exit criteria
|
||
|
||
Phase 4 is complete only when:
|
||
|
||
- text-only OpenCode delivery path is unchanged;
|
||
- OpenCode image send is allowed only for supported/verified vision models;
|
||
- GLM 5.1 image send is blocked before OpenCode call;
|
||
- Kimi K2.6 and GLM 4.5V image sends serialize to file parts in tests;
|
||
- OpenCode delivery proof semantics are unchanged;
|
||
- provider auth/quota/model errors remain exact;
|
||
- no automatic live probe runs on normal send.
|
||
|
||
## Cross-repo sequencing
|
||
|
||
Recommended order:
|
||
|
||
1. Orchestrator: extend OpenCode bridge to accept typed `parts` while preserving text-only API.
|
||
2. Orchestrator: test file part serialization with fake client.
|
||
3. Desktop: add OpenCode capability resolver.
|
||
4. Desktop: add OpenCode adapter.
|
||
5. Desktop: wire direct OpenCode secondary attachment path.
|
||
6. Add UI warnings/blockers for unsupported/unknown models.
|
||
7. Run live smoke only after unit/fixture tests pass.
|
||
|
||
## Backward-compatible bridge API
|
||
|
||
Keep old text API as wrapper.
|
||
|
||
```ts
|
||
async sendPromptText(input: { sessionId: string; text: string }) {
|
||
return this.sendPromptParts({
|
||
sessionId: input.sessionId,
|
||
parts: [{ type: 'text', text: input.text }],
|
||
});
|
||
}
|
||
```
|
||
|
||
New API:
|
||
|
||
```ts
|
||
async sendPromptParts(input: { sessionId: string; parts: OpenCodeBridgePromptPart[] }) {
|
||
validateParts(input.parts);
|
||
return this.client.sendSessionMessage(input.sessionId, { parts: input.parts });
|
||
}
|
||
```
|
||
|
||
## Capability resolver detail
|
||
|
||
Return both machine code and user copy.
|
||
|
||
```ts
|
||
export interface OpenCodeVisionCapabilityDecision {
|
||
kind: 'supported' | 'unsupported' | 'unknown';
|
||
code:
|
||
| 'opencode_vision_supported'
|
||
| 'opencode_model_text_only'
|
||
| 'opencode_model_vision_unknown';
|
||
source: 'curated' | 'heuristic' | 'provider-metadata';
|
||
userMessage?: string;
|
||
diagnostic: string;
|
||
}
|
||
```
|
||
|
||
This prevents UI from string-matching diagnostics.
|
||
|
||
## Provider auth vs capability
|
||
|
||
Do not require provider auth to decide static capability. A model can be known vision-capable even if auth is missing. Send still fails/preflights on auth separately.
|
||
|
||
Example:
|
||
|
||
```text
|
||
openrouter/moonshotai/kimi-k2.6 capability = supported
|
||
OpenRouter auth missing = provider setup blocker
|
||
```
|
||
|
||
Both may appear in UI, but auth blocker is operational and capability is model-level.
|
||
|
||
## More OpenCode tests
|
||
|
||
```ts
|
||
it('keeps text-only API as wrapper around parts API', async () => {
|
||
await bridge.sendPromptText({ sessionId: 's', text: 'hello' });
|
||
expect(client.sendSessionMessage).toHaveBeenCalledWith('s', {
|
||
parts: [{ type: 'text', text: 'hello' }],
|
||
});
|
||
});
|
||
```
|
||
|
||
```ts
|
||
it('serializes image file part without changing response observer', async () => {
|
||
await bridge.sendPromptParts({
|
||
sessionId: 's',
|
||
parts: [
|
||
{ type: 'text', text: 'what color?' },
|
||
{ type: 'file', mime: 'image/png', url: 'data:image/png;base64,AAAA', filename: 'x.png' },
|
||
],
|
||
});
|
||
expect(observer).not.toHaveNewSuccessRule();
|
||
});
|
||
```
|
||
|
||
## More OpenCode bug traps
|
||
|
||
| Trap | Prevention |
|
||
|---|---|
|
||
| unknown model allowed and silently fails | unknown blocks by default |
|
||
| bridge accepts arbitrary part JSON | strict union validation |
|
||
| file part accepted marks ledger delivered | tests assert proof unchanged |
|
||
| provider key logged in smoke | redactor in smoke script |
|
||
| model capability string hardcoded in UI only | backend resolver is source of truth |
|
||
| OpenRouter model id normalization inconsistent | shared `normalizeOpenCodeModelRef` tests |
|
||
|
||
## Detailed Implementation Checklist
|
||
|
||
### Step 1 - Add OpenCode capability matrix
|
||
|
||
OpenCode support depends on provider and model, not just provider id.
|
||
|
||
```ts
|
||
export interface OpenCodeModelVisionCapability {
|
||
provider: 'openai' | 'openrouter' | string;
|
||
modelPattern: RegExp;
|
||
vision: 'yes' | 'no' | 'unknown';
|
||
evidence: 'live-smoke' | 'docs' | 'manual' | 'default';
|
||
}
|
||
```
|
||
|
||
Initial known evidence from prototype:
|
||
|
||
- `openai/gpt-5.4-mini`: vision yes.
|
||
- `openrouter/moonshotai/kimi-k2.6`: vision yes.
|
||
- `openrouter/z-ai/glm-4.5v`: vision yes.
|
||
- `openrouter/z-ai/glm-5.1`: vision no or unreliable for images based on smoke result.
|
||
|
||
Unknown models should not silently accept images. Use warning/block depending on UX decision:
|
||
|
||
- For release-safe behavior: block unknown vision models and explain.
|
||
- For exploratory dev behavior: allow with explicit “model may not support images” warning only if user confirms. Not recommended before release.
|
||
|
||
### Step 2 - Route OpenCode attachments through file parts
|
||
|
||
Use OpenCode file attachment mechanism, not base64 text.
|
||
|
||
Conceptual orchestrator command:
|
||
|
||
```ts
|
||
const args = ['run', '--format', 'json', '--model', model, prompt];
|
||
for (const image of request.images) {
|
||
args.push('-f', image.path);
|
||
}
|
||
```
|
||
|
||
If using HTTP/server API instead of CLI, keep the same contract: file path/part is transport-native.
|
||
|
||
### Step 3 - Integrate with delivery ledger safely
|
||
|
||
Attachment delivery result must still satisfy the existing prompt delivery proof rules.
|
||
|
||
| Result | Ledger behavior |
|
||
|---|---|
|
||
| Model returns visible reply | Existing success path. |
|
||
| Model returns plain text direct user reply | Existing safe materialization path. |
|
||
| Model says it cannot view image | Delivery succeeded, semantic unsupported response. Mark delivered if visible reply exists. |
|
||
| Provider rejects file | Delivery failed with provider diagnostic. |
|
||
| Empty assistant turn | Existing bounded repair policy applies. |
|
||
| Non-visible tool without progress | Existing proof-directed repair applies. |
|
||
|
||
### Step 4 - Add exact diagnostics
|
||
|
||
OpenCode attachment diagnostics should include provider/model context but redact secrets.
|
||
|
||
Examples:
|
||
|
||
- `OpenCode model openrouter/z-ai/glm-5.1 is not marked vision-capable for image attachments.`
|
||
- `OpenCode image artifact missing before delivery.`
|
||
- `OpenCode provider rejected image attachment: <redacted error>`
|
||
- `OpenCode returned a visible reply saying it cannot inspect images.`
|
||
|
||
Do not turn a model “I cannot see images” answer into transport failure if the model produced a visible answer. That is useful user feedback.
|
||
|
||
## OpenCode Edge Cases
|
||
|
||
| Case | Expected behavior |
|
||
|---|---|
|
||
| OpenRouter key quota exceeded | Provider failure with exact redacted diagnostic. |
|
||
| OpenCode OAuth stale | Auth/session failure, not attachment failure. |
|
||
| Model vision unknown | Block or warn according to release setting, default block. |
|
||
| Model claims no image support despite capability yes | Visible semantic answer, not retry-loop. |
|
||
| File path accepted but model returns empty turn | Existing bounded empty-turn repair. |
|
||
| Concurrent messages with images | Ledger correlation by original message id remains authoritative. |
|
||
| Retry of failed image delivery | Reuse same managed artifact if still present, otherwise fail artifact missing. |
|
||
| User restarts member after image failure | Restart does not delete inbox message or attachment metadata. |
|
||
|
||
## Capability Governance
|
||
|
||
Do not scatter model lists across UI and backend.
|
||
|
||
Recommended single source:
|
||
|
||
```text
|
||
src/features/agent-attachments/shared/opencodeVisionCapabilities.ts
|
||
```
|
||
|
||
Export both backend and renderer-safe functions:
|
||
|
||
```ts
|
||
export function resolveOpenCodeVisionCapability(input: {
|
||
providerId: string;
|
||
model: string;
|
||
}): AgentAttachmentCapability {
|
||
// Pure function, no filesystem, no network.
|
||
}
|
||
```
|
||
|
||
When adding a new model:
|
||
|
||
1. Add capability entry with evidence comment.
|
||
2. Add unit test.
|
||
3. Add manual smoke result to this plan or docs.
|
||
4. Do not infer full provider support from one model.
|
||
|
||
## Manual OpenCode QA
|
||
|
||
Run at least these visual prompts:
|
||
|
||
1. `openai/gpt-5.4-mini`: red-card image -> expected `red`.
|
||
2. `openrouter/moonshotai/kimi-k2.6`: red-card image -> expected `red`.
|
||
3. `openrouter/z-ai/glm-4.5v`: red-card image -> expected `red`.
|
||
4. `openrouter/z-ai/glm-5.1`: red-card image -> expected block or clear unsupported warning.
|
||
5. Quota/key failure simulation -> exact provider error visible, no teammate offline unless runtime exits.
|
||
|
||
## Phase 4 Exit Criteria
|
||
|
||
- OpenCode text-only prompt path unchanged.
|
||
- Vision-capable OpenCode models receive real image files.
|
||
- Non-vision/unknown OpenCode models do not silently hallucinate image answers.
|
||
- Existing OpenCode repair policy remains bounded.
|
||
- Provider quota/auth errors remain exact and do not become generic “spawn failed”.
|
||
|
||
|
||
## Implementation Safeguards
|
||
|
||
### Capability result must be explainable
|
||
|
||
Users need to understand why one OpenCode model supports screenshots and another does not.
|
||
|
||
```ts
|
||
export interface CapabilityExplanation {
|
||
supported: boolean;
|
||
reason: 'known_vision_model' | 'known_non_vision_model' | 'unknown_model' | 'unsupported_provider';
|
||
displayText: string;
|
||
}
|
||
```
|
||
|
||
Examples:
|
||
|
||
- `openai/gpt-5.4-mini supports image attachments.`
|
||
- `openrouter/z-ai/glm-5.1 is not marked as image-capable. Choose a vision-capable model or remove images.`
|
||
- `This OpenCode model has unknown image support. Image delivery is blocked for reliability.`
|
||
|
||
### Do not couple OpenCode vision support to weak-model repair
|
||
|
||
The proof-directed repair policy handles missing response proof. It should not decide model vision support.
|
||
|
||
Keep separate:
|
||
|
||
- `OpenCodeVisionCapability`: can this model receive images?
|
||
- `OpenCodePromptDeliveryRepairPolicy`: did a delivered prompt produce required proof?
|
||
- `OpenCodeDeliveryLedger`: what happened to this message attempt?
|
||
|
||
### OpenCode provider rejection mapping
|
||
|
||
OpenRouter/OpenCode errors should remain exact but redacted.
|
||
|
||
| Provider error | Mapping |
|
||
|---|---|
|
||
| Quota/credits/token budget | provider rejected attachment/message, exact diagnostic. |
|
||
| Model not found | model/provider config diagnostic. |
|
||
| Unsupported file/input | attachment provider rejected. |
|
||
| OAuth/session stale | provider auth/session diagnostic. |
|
||
| Empty assistant turn | existing bounded no-response repair. |
|
||
|
||
No regex classification is required to decide business logic. Regex/string matching can be used only for safe redaction and known diagnostic display cleanup.
|
||
|
||
### OpenCode PR checklist
|
||
|
||
- Unknown models do not silently receive images.
|
||
- Supported models use native file parts.
|
||
- No model-specific prompt hacks are added.
|
||
- Existing text-only OpenCode tests still pass.
|
||
- Existing empty-turn retry remains bounded.
|
||
- Provider quota/auth errors are not masked by attachment code.
|
||
|
||
### Additional OpenCode tests
|
||
|
||
```ts
|
||
it('blocks unknown OpenCode vision model before delivery', () => {
|
||
const decision = resolveOpenCodeVisionCapability({
|
||
providerId: 'openrouter',
|
||
model: 'some/new-model',
|
||
});
|
||
|
||
expect(decision.supported).toBe(false);
|
||
expect(decision.reason).toBe('unknown_model');
|
||
});
|
||
|
||
it('does not invoke repair policy for model capability block', () => {
|
||
const result = planOpenCodeAttachmentDelivery(unknownVisionModelInput());
|
||
expect(result.status).toBe('blocked');
|
||
expect(result.retryable).toBe(false);
|
||
});
|
||
```
|
||
|
||
|
||
## Failure Injection Tests for Phase 4
|
||
|
||
```ts
|
||
describe('OpenCode image capability and delivery', () => {
|
||
it('blocks a known non-vision OpenCode model before prompt delivery', () => {
|
||
const result = planOpenCodeImageDelivery({
|
||
providerId: 'openrouter',
|
||
model: 'z-ai/glm-5.1',
|
||
attachments: [fakeImageAttachment()],
|
||
});
|
||
|
||
expect(result.status).toBe('blocked');
|
||
expect(result.failureCode).toBe('attachment_model_unsupported');
|
||
});
|
||
|
||
it('does not retry provider quota rejection as proof repair', async () => {
|
||
const result = await deliverOpenCodeImage(fakeOpenRouterQuotaError());
|
||
|
||
expect(result.retryKind).not.toBe('proof_directed_repair');
|
||
expect(result.failureCode).toBe('attachment_provider_rejected');
|
||
});
|
||
});
|
||
```
|
||
|
||
## OpenCode Multi-Hop Edge Cases
|
||
|
||
| Scenario | Safe behavior |
|
||
|---|---|
|
||
| User sends image to lead, lead delegates to OpenCode member | Lead message contains image; delegated message should include image only if explicitly attached/forwarded by lead protocol. Do not automatically leak user image to all teammates. |
|
||
| User sends direct image to OpenCode member | Direct delivery uses OpenCode file parts. |
|
||
| OpenCode member replies with tool call but no visible reply | Existing proof policy decides, not image layer. |
|
||
| OpenCode member starts task from image | Task evidence/progress still required for task state. |
|
||
| OpenCode model answers “I cannot access images” | Visible reply delivered; UI can show model limitation, no retry loop. |
|
||
|
||
## Privacy Boundary for Delegation
|
||
|
||
Do not automatically fan out attachments to every teammate. Attachment propagation should follow explicit message routing.
|
||
|
||
Rules:
|
||
|
||
- Direct `user -> member`: deliver attachment to that member only.
|
||
- `user -> lead`: deliver attachment to lead only.
|
||
- Lead delegation: attachment forwarded only if the generated structured message includes or references the attachment intentionally.
|
||
- System nudges/work-sync: do not include user attachments.
|
||
|
||
This avoids accidental data exposure across teammates.
|
||
|
||
## Phase 4 Stop Conditions
|
||
|
||
Stop and reassess if:
|
||
|
||
- OpenCode requires model-specific prompt hacks to see images.
|
||
- Unknown models must be allowed silently for UX convenience.
|
||
- Existing proof repair tests start changing because of image capability logic.
|
||
- Provider quota errors get converted into generic delivery proof failures.
|
||
|
||
|
||
## File-Level Implementation Plan
|
||
|
||
Desktop suggested files:
|
||
|
||
```text
|
||
src/features/agent-attachments/main/providers/opencodeAttachmentAdapter.ts
|
||
src/features/agent-attachments/shared/opencodeVisionCapabilities.ts
|
||
src/features/agent-attachments/main/providers/opencodeAttachmentAdapter.test.ts
|
||
```
|
||
|
||
Orchestrator suggested files:
|
||
|
||
```text
|
||
src/runtime/opencode/opencodeFileParts.ts
|
||
src/runtime/opencode/opencodeFileParts.test.ts
|
||
```
|
||
|
||
### Capability matrix skeleton
|
||
|
||
```ts
|
||
const OPENCODE_VISION_MODELS: OpenCodeModelVisionCapability[] = [
|
||
{
|
||
provider: 'openai',
|
||
modelPattern: /^gpt-5\.4-mini$/,
|
||
vision: 'yes',
|
||
evidence: 'live-smoke',
|
||
},
|
||
{
|
||
provider: 'openrouter',
|
||
modelPattern: /^moonshotai\/kimi-k2\.6$/,
|
||
vision: 'yes',
|
||
evidence: 'live-smoke',
|
||
},
|
||
{
|
||
provider: 'openrouter',
|
||
modelPattern: /^z-ai\/glm-4\.5v$/,
|
||
vision: 'yes',
|
||
evidence: 'live-smoke',
|
||
},
|
||
{
|
||
provider: 'openrouter',
|
||
modelPattern: /^z-ai\/glm-5\.1$/,
|
||
vision: 'no',
|
||
evidence: 'live-smoke',
|
||
},
|
||
];
|
||
```
|
||
|
||
Keep comments near each entry with date and smoke summary. Do not infer provider-wide support.
|
||
|
||
### OpenCode adapter skeleton
|
||
|
||
```ts
|
||
export function buildOpenCodeAttachmentPromptRequest(input: {
|
||
text: string;
|
||
providerId: string;
|
||
model: string;
|
||
attachments: AgentAttachmentPayload[];
|
||
}): OpenCodePromptRequest {
|
||
const capability = resolveOpenCodeVisionCapability({ providerId: input.providerId, model: input.model });
|
||
if (!capability.supported) {
|
||
throw new AttachmentDeliveryError('attachment_model_unsupported', capability.displayText);
|
||
}
|
||
|
||
return {
|
||
text: input.text,
|
||
files: input.attachments.map((attachment) => selectProviderImageArtifact(attachment, 'opencode').absolutePath),
|
||
};
|
||
}
|
||
```
|
||
|
||
### OpenCode warning copy examples
|
||
|
||
Use precise user-facing copy:
|
||
|
||
```text
|
||
This OpenCode model is not verified for image attachments. Choose a vision-capable model or remove the image.
|
||
```
|
||
|
||
```text
|
||
OpenCode provider rejected the image attachment: <redacted provider error>
|
||
```
|
||
|
||
Avoid vague copy:
|
||
|
||
```text
|
||
Message failed.
|
||
```
|
||
|
||
```text
|
||
OpenCode error.
|
||
```
|
||
|
||
|
||
## Phase 4 Deep Review Addendum
|
||
|
||
### OpenCode capability decision must be cached but refreshable
|
||
|
||
Model capability matrix is mostly static, but the user may change provider/model in Edit Team.
|
||
|
||
Rules:
|
||
|
||
- Resolve capability from current member metadata at send time.
|
||
- Do not cache capability per member forever.
|
||
- If member model changes, next send uses new model capability.
|
||
- UI warnings should refresh after Edit Team save/restart.
|
||
|
||
### OpenCode direct vs lead-routed images
|
||
|
||
Direct user to OpenCode member:
|
||
|
||
```text
|
||
user -> alice(OpenCode) with image
|
||
```
|
||
|
||
Deliver image to Alice only.
|
||
|
||
User to lead:
|
||
|
||
```text
|
||
user -> lead with image
|
||
```
|
||
|
||
Deliver image to lead only. Lead may choose to ask teammate, but automatic image forwarding should not happen unless future protocol explicitly supports attachment references.
|
||
|
||
This is a privacy and predictability decision.
|
||
|
||
### OpenCode unsupported model message examples
|
||
|
||
For direct send:
|
||
|
||
```text
|
||
Alice is using openrouter/z-ai/glm-5.1, which is not verified for image attachments. Remove the image or switch Alice to a vision-capable model.
|
||
```
|
||
|
||
For team send to lead where lead is not OpenCode:
|
||
|
||
```text
|
||
The lead can receive this image. It will not be automatically forwarded to OpenCode teammates unless the lead explicitly sends it.
|
||
```
|
||
|
||
### OpenCode model capability update procedure
|
||
|
||
When adding a new OpenCode vision model:
|
||
|
||
1. Run red-square smoke test.
|
||
2. Record provider, exact model id, date, result.
|
||
3. Add capability matrix entry.
|
||
4. Add unit test.
|
||
5. If model is unreliable, mark unsupported or unknown, not supported.
|
||
|
||
|
||
## Phase 4 Implementation Contract Addendum
|
||
|
||
### Provider/model canonicalization
|
||
|
||
OpenCode model ids can appear with or without provider prefix depending on UI/runtime source. Normalize before capability lookup.
|
||
|
||
```ts
|
||
export function canonicalizeOpenCodeModel(input: {
|
||
providerId: string;
|
||
model: string;
|
||
}): { providerId: string; model: string } {
|
||
const providerId = input.providerId.toLowerCase();
|
||
const model = input.model.replace(/^openrouter\//, '').replace(/^openai\//, '');
|
||
return { providerId, model };
|
||
}
|
||
```
|
||
|
||
Capability tests must cover both forms:
|
||
|
||
- `providerId=openrouter`, `model=moonshotai/kimi-k2.6`
|
||
- `providerId=openrouter`, `model=openrouter/moonshotai/kimi-k2.6`
|
||
|
||
### OpenCode DTO between desktop and orchestrator
|
||
|
||
```ts
|
||
export interface OpenCodePromptRuntimeRequest {
|
||
text: string;
|
||
files?: Array<{
|
||
artifactId: string;
|
||
path: string;
|
||
mimeType: 'image/png' | 'image/jpeg';
|
||
sizeBytes: number;
|
||
}>;
|
||
}
|
||
```
|
||
|
||
Rules:
|
||
|
||
- `files` are provider-native attachments, not arbitrary user paths.
|
||
- `files` empty means text-only behavior.
|
||
- Orchestrator validates path before passing `-f` or HTTP file part.
|
||
|
||
### OpenCode ledger interaction examples
|
||
|
||
```ts
|
||
if (capability.supported === false) {
|
||
return {
|
||
accepted: false,
|
||
delivered: false,
|
||
retryable: false,
|
||
failureCode: 'attachment_model_unsupported',
|
||
};
|
||
}
|
||
```
|
||
|
||
This must not create a ledger retry attempt because the prompt was not delivered.
|
||
|
||
If provider accepts prompt but returns empty assistant turn, existing ledger retry applies:
|
||
|
||
```ts
|
||
return {
|
||
accepted: true,
|
||
delivered: false,
|
||
retryable: true,
|
||
failureCode: 'empty_assistant_turn',
|
||
};
|
||
```
|
||
|
||
|
||
## Implementation Readiness Addendum
|
||
|
||
### Definition of Ready for Phase 4
|
||
|
||
Before coding Phase 4:
|
||
|
||
- Phase 1 normalized artifacts are stable.
|
||
- Existing OpenCode delivery ledger tests are green.
|
||
- OpenCode text-only direct reply path is green.
|
||
- Capability matrix initial entries are accepted.
|
||
- Orchestrator OpenCode file-part mechanism is identified.
|
||
|
||
### OpenCode mocked sender strategy
|
||
|
||
Use fake OpenCode sender outcomes:
|
||
|
||
```ts
|
||
type FakeOpenCodeOutcome =
|
||
| { kind: 'visible_reply'; text: string }
|
||
| { kind: 'empty_assistant_turn' }
|
||
| { kind: 'provider_error'; message: string }
|
||
| { kind: 'non_visible_tool' };
|
||
```
|
||
|
||
Test attachment adapter and ledger separately:
|
||
|
||
- Adapter tests: capability and file-part request building.
|
||
- Ledger tests: how delivery outcome affects retry/proof.
|
||
- Do not combine both in one huge brittle test unless it is e2e.
|
||
|
||
### OpenCode additional edge cases
|
||
|
||
| Edge case | Expected behavior |
|
||
|---|---|
|
||
| Model id casing differs | Canonicalize before capability lookup. |
|
||
| Provider prefix duplicated | Canonicalize. |
|
||
| OpenRouter key missing | Existing provider auth diagnostic. |
|
||
| OpenRouter credits exhausted | Provider diagnostic, no proof repair. |
|
||
| Vision model returns empty | Existing bounded empty-turn retry. |
|
||
| Non-vision model says cannot see image | If blocked before send, this should not occur. If it occurs from stale capability, visible reply is shown and capability should be corrected. |
|
||
|
||
### Capability matrix change control
|
||
|
||
Do not accept capability entries without evidence.
|
||
|
||
Required evidence for `vision: yes`:
|
||
|
||
- Real smoke prompt.
|
||
- Model id exactly as runtime uses it.
|
||
- Date.
|
||
- Observed answer.
|
||
|
||
Required evidence for `vision: no`:
|
||
|
||
- Real smoke or provider documentation.
|
||
- Observed refusal/unsupported behavior.
|
||
|
||
|
||
## Final Phase 4 Acceptance Specs
|
||
|
||
### Spec 1 - known vision OpenCode model
|
||
|
||
```gherkin
|
||
Given a user sends an image to openrouter/moonshotai/kimi-k2.6
|
||
And the capability matrix marks it vision-capable
|
||
When delivery is planned
|
||
Then the image is sent as a native file part
|
||
And existing ledger proof gates decide delivery success
|
||
```
|
||
|
||
### Spec 2 - known non-vision OpenCode model
|
||
|
||
```gherkin
|
||
Given a user sends an image to openrouter/z-ai/glm-5.1
|
||
And the capability matrix marks it unsupported
|
||
When delivery is planned
|
||
Then delivery is blocked before prompt send
|
||
And no retry ledger attempt is created
|
||
And the UI explains that the model is not verified for image attachments
|
||
```
|
||
|
||
### Spec 3 - OpenCode provider quota error
|
||
|
||
```gherkin
|
||
Given an OpenCode provider rejects a message due to quota
|
||
When an image message is sent
|
||
Then the exact redacted provider diagnostic is shown
|
||
And the failure is not converted into proof repair
|
||
And the member launch state is unchanged
|
||
```
|
||
|
||
### Phase 4 exact PR contract
|
||
|
||
The Phase 4 PR is acceptable only if:
|
||
|
||
- Capability matrix has tests for every entry.
|
||
- Unknown models default safe.
|
||
- File-part request builder has tests.
|
||
- Existing OpenCode ledger bounded retry tests remain green.
|
||
- Unsupported capability block does not create a retry attempt.
|
||
- Direct and lead-routed privacy rules are documented in tests or code comments.
|
||
|
||
### OpenCode capability copy examples
|
||
|
||
```text
|
||
Jack is using openrouter/z-ai/glm-5.1, which is not verified for image attachments. Switch to a vision-capable model or remove the image.
|
||
```
|
||
|
||
```text
|
||
Alice can receive this image because openai/gpt-5.4-mini is verified for image attachments.
|
||
```
|
||
|
||
|
||
## Phase 4 Pre-Mortem and Extra Safeguards
|
||
|
||
### Likely OpenCode mistakes
|
||
|
||
| Mistake | Concrete prevention |
|
||
|---|---|
|
||
| Unknown model allowed because “maybe vision” | Unknown defaults blocked. |
|
||
| Capability lookup misses provider prefix variant | Canonicalization tests. |
|
||
| Provider quota error enters proof repair | Provider rejection is terminal for that attempt, not proof repair. |
|
||
| Direct user image gets auto-forwarded to teammates | Explicit route-only attachment propagation. |
|
||
| Non-vision model gets image-less prompt silently | Capability block before delivery. |
|
||
|
||
### OpenCode file-part transport abstraction
|
||
|
||
Do not let high-level delivery know whether OpenCode uses CLI `-f` or HTTP multipart internally.
|
||
|
||
```ts
|
||
export interface OpenCodeFilePartTransport {
|
||
sendPrompt(input: {
|
||
sessionId: string;
|
||
text: string;
|
||
files: OpenCodeRuntimeFilePart[];
|
||
}): Promise<OpenCodePromptOutcome>;
|
||
}
|
||
```
|
||
|
||
This allows future switch from CLI to server API without changing capability or ledger logic.
|
||
|
||
### OpenCode semantic refusal handling
|
||
|
||
If model responds visibly with “I cannot inspect images”:
|
||
|
||
- Direct user message: show response as delivered.
|
||
- Add optional diagnostic warning that capability matrix may be wrong.
|
||
- Do not retry automatically.
|
||
- Consider downgrading capability entry after repeated smoke failure.
|
||
|
||
### OpenCode tests for no silent drop
|
||
|
||
```ts
|
||
it('does not send text-only prompt when image is unsupported', async () => {
|
||
const sender = createFakeOpenCodeSender();
|
||
await expect(deliverImageToUnsupportedOpenCodeModel(sender)).rejects.toMatchObject({
|
||
code: 'attachment_model_unsupported',
|
||
});
|
||
expect(sender.calls).toHaveLength(0);
|
||
});
|
||
```
|
||
|