diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 1e2bfd46c..31c8aaf35 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -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(), diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 938139835..dc1d0dfca 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -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) */ diff --git a/packages/coding-agent/src/utils/image-loading.ts b/packages/coding-agent/src/utils/image-loading.ts index c643b626f..d2b33b509 100644 --- a/packages/coding-agent/src/utils/image-loading.ts +++ b/packages/coding-agent/src/utils/image-loading.ts @@ -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 | 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 | 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 { 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