Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/__tests__/catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
buildThinkingConfig,
contextWindowFromCatalog,
DEFAULT_CONTEXT_WINDOW,
getCachedModelConfig,
Expand Down Expand Up @@ -169,3 +170,57 @@ describe("ZERO_COST", () => {
expect(Object.isFrozen(ZERO_COST)).toBe(true);
});
});

describe("buildThinkingConfig", () => {
it("maps an effort-based model with a disabled branch", () => {
const entry = {
thinking_config: {
disabled: { description: "Disable thinking" },
enabled: {
efforts: { high: {}, max: { is_default: true } },
is_default: true,
},
},
};
expect(buildThinkingConfig(entry)).toEqual({
mode: "effort",
efforts: ["high", "max"],
defaultLevel: "max",
requiresEffort: false,
});
});

it("marks reasoning mandatory when there is no disabled branch", () => {
const entry = {
thinking_config: {
enabled: {
efforts: { high: {}, max: { is_default: true } },
is_default: true,
},
},
};
expect(buildThinkingConfig(entry)).toMatchObject({
mode: "effort",
efforts: ["high", "max"],
defaultLevel: "max",
requiresEffort: true,
});
});

it("orders efforts least to most intensive", () => {
const entry = {
thinking_config: {
enabled: {
efforts: { xhigh: {}, high: { is_default: true }, low: {}, max: {}, medium: {} },
is_default: true,
},
},
};
expect(buildThinkingConfig(entry)?.efforts).toEqual(["low", "medium", "high", "xhigh", "max"]);
});

it("returns undefined for toggle=only models and non-reasoning entries", () => {
expect(buildThinkingConfig({ thinking_config: { enabled: { is_default: true } } })).toBeUndefined();
expect(buildThinkingConfig({})).toBeUndefined();
});
});
47 changes: 47 additions & 0 deletions src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ export interface QoderModelEntry {
[key: string]: unknown;
}

/**
* omp fork's `ThinkingConfig` shape. The fork reads `model.thinking` (not the
* `thinkingLevelMap` that earendil pi-ai exposes), and when it is absent omp
* fills a generic default ladder (`minimal`..`high`) instead of the model's
* real efforts - so `DeepSeek-V4-Pro` would show `low`/`medium` it does not
* support. Defining this inline because the plugin compiles against
* @earendil-works/pi-ai, which has no such field.
*/
export interface QoderThinkingConfig {
mode: "effort";
efforts: readonly ThinkingLevel[];
defaultLevel?: ThinkingLevel;
requiresEffort?: boolean;
}

export interface QoderModelDef {
id: string;
upstreamKey?: string;
Expand All @@ -58,6 +73,7 @@ export interface QoderModelDef {
reasoning: boolean;
supportsEffort: boolean;
thinkingLevelMap?: ThinkingLevelMap;
thinking?: QoderThinkingConfig;
input: ("text" | "image")[];
cost: typeof ZERO_COST;
contextWindow: number;
Expand Down Expand Up @@ -526,6 +542,35 @@ function buildThinkingLevelMap(entry: QoderModelEntry): ThinkingLevelMap | undef
return undefined;
}

/**
* Map Qoder's `thinking_config` to omp's `thinking` metadata so omp's effort
* picker lists the actual supported levels (it reads `thinking.efforts`, not
* `thinkingLevelMap`). Only effort-based models have a meaningful ladder;
* toggle-only models (no `efforts`) return undefined so omp falls back to its
* generic behavior.
*/
export function buildThinkingConfig(entry: QoderModelEntry): QoderThinkingConfig | undefined {
const tc = entry.thinking_config;
const efforts = tc?.enabled?.efforts;
if (!efforts || typeof efforts !== "object") return undefined;

const supported = new Set(Object.keys(efforts));
const ordered = PI_THINKING_LEVELS.filter((level) => supported.has(level));
if (ordered.length === 0) return undefined;

const defaultLevel = PI_THINKING_LEVELS.find((level) => efforts[level]?.is_default);

return {
mode: "effort",
efforts: ordered,
...(defaultLevel ? { defaultLevel } : {}),
// Qoder's catalog marks a `disabled` branch when thinking can be turned
// off. Without it reasoning is mandatory upstream, so omp clamps "off" to
// the lowest supported effort instead of disabling.
requiresEffort: !tc?.disabled,
};
}

export function getCachedModels(mode: QoderMode): QoderModelDef[] {
const data = readParsedModelCache(mode);
if (data && Array.isArray(data.models)) {
Expand Down Expand Up @@ -678,6 +723,7 @@ export async function updateQoderModelsCache(
const isReasoning = !!entry.is_reasoning || !!entry.thinking_config;
const supportsEffort = !!entry.thinking_config?.enabled?.efforts;
const thinkingLevelMap = buildThinkingLevelMap(entry);
const thinking = buildThinkingConfig(entry);
// Both regions expose display_name (whitespace-stripped) as the sole
// pi-visible id. The config stores the upstream key under that id for
// request-time use.
Expand All @@ -694,6 +740,7 @@ export async function updateQoderModelsCache(
reasoning: isReasoning,
supportsEffort,
thinkingLevelMap,
thinking,
input: isVL ? ["text", "image"] : ["text"],
cost: ZERO_COST,
contextWindow: ctxLen,
Expand Down
Loading