feat: added model-aware image handling for session tools and webp
- Added getActiveModel support to session/tool interfaces for propagating active model objects. - Added model capability helpers to flag WebP-unfriendly Ollama backends for image resize options. - Updated image normalization and loading to auto-disable/reencode WebP when model constraints require it.
This commit is contained in:
@@ -1492,6 +1492,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
getSessionSpawns: () => options.spawns ?? "*",
|
||||
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
||||
getActiveModelString,
|
||||
getActiveModel: () => agent?.state.model ?? model,
|
||||
getPlanModeState: () => session?.getPlanModeState(),
|
||||
getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
|
||||
getGoalModeState: () => session?.getGoalModeState(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
||||
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { FetchImpl, ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import type { FetchImpl, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJobManager } from "../async/job-manager";
|
||||
import type { Rule } from "../capability/rule";
|
||||
@@ -217,6 +217,8 @@ export interface ToolSession {
|
||||
getModelString?: () => string | undefined;
|
||||
/** Get the current session model string, regardless of how it was chosen */
|
||||
getActiveModelString?: () => string | undefined;
|
||||
/** Get the current session model object (provider/api capabilities), regardless of how it was chosen. */
|
||||
getActiveModel?: () => Model | undefined;
|
||||
/** Auth storage for passing to subagents (avoids re-discovery) */
|
||||
authStorage?: import("../session/auth-storage").AuthStorage;
|
||||
/** Model registry for passing to subagents (avoids re-discovery) */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
||||
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
|
||||
import { resolveReadPath } from "../tools/path-utils";
|
||||
import { formatDimensionNote, type ImageResizeOptions, resizeImage } from "./image-resize";
|
||||
@@ -7,6 +7,26 @@ import { formatDimensionNote, type ImageResizeOptions, resizeImage } from "./ima
|
||||
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
||||
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
||||
|
||||
/**
|
||||
* Ollama and its local-backend family decode image input through llama.cpp /
|
||||
* `stb_image`, which is compiled without WebP support, so a WebP upload fails
|
||||
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
||||
* to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
|
||||
*/
|
||||
export function modelLacksWebpSupport(model: Pick<Model, "provider" | "api"> | undefined): boolean {
|
||||
if (!model) return false;
|
||||
return model.provider === "ollama" || model.provider === "ollama-cloud" || model.api === "ollama-chat";
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` when `model` cannot decode WebP, otherwise `undefined` so the
|
||||
* `OMP_NO_WEBP` env fallback in {@link resizeImage} still applies. Feed straight
|
||||
* into {@link ImageResizeOptions.excludeWebP}.
|
||||
*/
|
||||
export function webpExclusionForModel(model: Pick<Model, "provider" | "api"> | undefined): true | undefined {
|
||||
return modelLacksWebpSupport(model) ? true : undefined;
|
||||
}
|
||||
|
||||
export interface LoadImageInputOptions {
|
||||
path: string;
|
||||
cwd: string;
|
||||
@@ -14,6 +34,8 @@ export interface LoadImageInputOptions {
|
||||
maxBytes?: number;
|
||||
resolvedPath?: string;
|
||||
detectedMimeType?: string;
|
||||
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
|
||||
excludeWebP?: boolean;
|
||||
}
|
||||
|
||||
export interface LoadedImageInput {
|
||||
@@ -51,6 +73,8 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
|
||||
}
|
||||
|
||||
export interface NormalizeModelContextImagesOptions {
|
||||
/** Model the images are bound for; used to derive encoder constraints (WebP exclusion for Ollama). */
|
||||
model?: Model;
|
||||
resize?: ImageResizeOptions;
|
||||
}
|
||||
|
||||
@@ -66,10 +90,13 @@ export async function normalizeModelContextImages(
|
||||
options?: NormalizeModelContextImagesOptions,
|
||||
): Promise<ImageContent[] | undefined> {
|
||||
if (!images || images.length === 0) return undefined;
|
||||
const resize: ImageResizeOptions | undefined = modelLacksWebpSupport(options?.model)
|
||||
? { ...options?.resize, excludeWebP: true }
|
||||
: options?.resize;
|
||||
const normalized: ImageContent[] = [];
|
||||
for (const image of images) {
|
||||
try {
|
||||
const resized = await resizeImage(image, options?.resize);
|
||||
const resized = await resizeImage(image, resize);
|
||||
normalized.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
||||
} catch {
|
||||
// Preserve existing caller behavior for decode/resize failures: keep the
|
||||
@@ -104,9 +131,13 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
|
||||
let outputBytes = inputBuffer.byteLength;
|
||||
let dimensionNote: string | undefined;
|
||||
|
||||
if (options.autoResize) {
|
||||
const shouldReencodeWebP = options.excludeWebP === true && mimeType === "image/webp";
|
||||
if (options.autoResize || shouldReencodeWebP) {
|
||||
try {
|
||||
const resized = await resizeImage({ type: "image", data: outputData, mimeType });
|
||||
const resized = await resizeImage(
|
||||
{ type: "image", data: outputData, mimeType },
|
||||
{ excludeWebP: options.excludeWebP },
|
||||
);
|
||||
outputData = resized.data;
|
||||
outputMimeType = resized.mimeType;
|
||||
outputBytes = resized.buffer.byteLength;
|
||||
|
||||
Reference in New Issue
Block a user