fix(catalog): marked cerebras gemma vision capable

Mapped Cerebras gemma-4-31b dynamic discovery to include image input capability and covered the OpenAI Chat Completions image_url serialization path.

Fixes #3854
This commit is contained in:
roboomp
2026-06-30 02:48:08 +00:00
parent 5c695e9e42
commit 2b944114fb
4 changed files with 121 additions and 1 deletions
@@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test";
import { convertMessages } from "@oh-my-pi/pi-ai/providers/openai-completions";
import { NON_VISION_IMAGE_PLACEHOLDER } from "@oh-my-pi/pi-ai/providers/vision-guard";
import type { AssistantMessage, Context, Model, ToolResultMessage, Usage } from "@oh-my-pi/pi-ai/types";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
@@ -75,6 +76,48 @@ function buildToolResult(toolCallId: string, timestamp: number): ToolResultMessa
}
describe("openai-completions convertMessages", () => {
it("serializes Cerebras gemma image inputs as Chat Completions data URIs", () => {
const model = buildModel({
id: "gemma-4-31b",
name: "Gemma 4 31B",
api: "openai-completions",
provider: "cerebras",
baseUrl: "https://api.cerebras.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
});
const context: Context = {
messages: [
{
role: "user",
content: [
{ type: "text", text: "Identify the shapes and colors. Return JSON only." },
{ type: "image", mimeType: "image/png", data: "ZmFrZQ==" },
],
timestamp: 1,
},
],
};
const messages = convertMessages(model, context, compat);
expect(messages).toEqual([
{
role: "user",
content: [
{ type: "text", text: "Identify the shapes and colors. Return JSON only." },
{
type: "image_url",
image_url: { url: "data:image/png;base64,ZmFrZQ==" },
},
],
},
]);
});
it("batches tool-result images after consecutive tool results", () => {
const baseModel = getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">;
const model: Model<"openai-completions"> = {
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Cerebras `gemma-4-31b` dynamic discovery to mark the model as image-capable so attached images are serialized as OpenAI Chat Completions `image_url` data URIs. ([#3854](https://github.com/can1357/oh-my-pi/issues/3854))
## [16.2.6] - 2026-06-29
### Fixed
@@ -803,6 +803,18 @@ export function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelM
// 3. Cerebras
// ---------------------------------------------------------------------------
const CEREBRAS_IMAGE_INPUT_MODEL_IDS = new Set(["gemma-4-31b"]);
function applyCerebrasDiscoveryOverrides(model: ModelSpec<"openai-completions">): ModelSpec<"openai-completions"> {
if (!CEREBRAS_IMAGE_INPUT_MODEL_IDS.has(model.id)) {
return model;
}
return {
...model,
input: ["text", "image"],
};
}
export interface CerebrasModelManagerConfig {
apiKey?: string;
baseUrl?: string;
@@ -812,7 +824,27 @@ export interface CerebrasModelManagerConfig {
export function cerebrasModelManagerOptions(
config?: CerebrasModelManagerConfig,
): ModelManagerOptions<"openai-completions"> {
return createSimpleOpenAICompletionsOptions("cerebras", "https://api.cerebras.ai/v1", config);
const apiKey = config?.apiKey;
const baseUrl = config?.baseUrl ?? "https://api.cerebras.ai/v1";
const references = createBundledReferenceMap<"openai-completions">("cerebras");
return {
providerId: "cerebras",
...(apiKey && {
fetchDynamicModels: () =>
fetchOpenAICompatibleModels({
api: "openai-completions",
provider: "cerebras",
baseUrl,
apiKey,
mapModel: (entry, defaults) => {
const reference = references.get(defaults.id);
const model = mapWithBundledReference(entry, defaults, reference);
return applyCerebrasDiscoveryOverrides(model);
},
fetch: config?.fetch,
}),
}),
};
}
// ---------------------------------------------------------------------------
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { cerebrasModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
import type { FetchImpl } from "@oh-my-pi/pi-catalog/types";
describe("Cerebras provider discovery", () => {
test("discovers gemma-4-31b as image-capable", async () => {
const calls: Array<{ url: string; authorization: string | null }> = [];
const fetchMock: FetchImpl = async (input: string | URL | Request, init?: RequestInit) => {
const headers = new Headers(init?.headers);
calls.push({
url: String(input),
authorization: headers.get("authorization"),
});
return new Response(
JSON.stringify({
data: [
{ id: "gemma-4-31b", object: "model" },
{ id: "llama3.1-8b", object: "model" },
],
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
};
const options = cerebrasModelManagerOptions({ apiKey: "cerebras-test-key", fetch: fetchMock });
const models = await options.fetchDynamicModels?.();
expect(calls).toEqual([
{
url: "https://api.cerebras.ai/v1/models",
authorization: "Bearer cerebras-test-key",
},
]);
expect(models?.find(model => model.id === "gemma-4-31b")).toMatchObject({
provider: "cerebras",
api: "openai-completions",
input: ["text", "image"],
});
expect(models?.find(model => model.id === "llama3.1-8b")?.input).toEqual(["text"]);
});
});