fix(gui): harden multimodal image attachments

This commit is contained in:
Yichen Jiang
2026-07-23 19:38:37 +08:00
parent eea595fcb4
commit 580e05b794
61 changed files with 1700 additions and 214 deletions

View File

@@ -25,8 +25,8 @@ import { toStreamChunks } from './stream.ts'
export interface PiAiAdapterOptions {
/** Validated provider profiles this adapter instance owns. */
profiles: readonly PiAiProviderProfile[]
/** Durable image resolver used only when a request contains image references. */
attachments?: AttachmentStore
/** Resolve durable image storage at request time so plugin load order does not become capability state. */
resolveAttachments?: () => AttachmentStore | undefined
}
/**
@@ -72,12 +72,12 @@ function requestHeaders(headers: Readonly<Record<string, string>> | undefined):
*/
export class PiAiAdapter extends LlmAdapter {
private readonly profiles: ReadonlyMap<string, ResolvedPiAiProviderProfile>
private readonly attachments: AttachmentStore | undefined
private readonly resolveAttachments: () => AttachmentStore | undefined
constructor(options: PiAiAdapterOptions) {
super()
this.profiles = new Map(resolveProfiles(options.profiles).map(profile => [profile.provider, profile]))
this.attachments = options.attachments
this.resolveAttachments = options.resolveAttachments ?? (() => undefined)
}
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
@@ -136,12 +136,13 @@ export class PiAiAdapter extends LlmAdapter {
if (containsImage && !model.input.includes('image')) {
throw new LlmError(`pi-ai model "${model.id}" does not support image input`, 'UNSUPPORTED_CONTENT')
}
if (containsImage && this.attachments === undefined) {
const attachments = containsImage ? this.resolveAttachments() : undefined
if (containsImage && attachments === undefined) {
throw new LlmError('pi-ai image input requires the durable attachment service', 'UNSUPPORTED_CONTENT')
}
const context = this.attachments === undefined
const context = attachments === undefined
? toPiContext(options)
: await toPiContext(options, this.attachments)
: await toPiContext(options, attachments)
const events = streamSimple(model, context, {
...profileOptions(profile),
...options.temperature === undefined ? {} : { temperature: options.temperature },

View File

@@ -36,10 +36,9 @@ export const inject = ['llm']
/** Register one generic pi-ai adapter for all configured provider routes. */
export function apply(ctx: Context, config: Config): void {
const profiles = resolveProfiles(config.providers)
const attachments = ctx.get('attachments')
const adapter = new PiAiAdapter({
profiles,
...(attachments === undefined ? {} : { attachments }),
resolveAttachments: () => ctx.get('attachments'),
})
ctx.llm.registerAdapter(profiles.map(entry => entry.provider), adapter)
}