mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(omp): limit thinking levels to model's reported efforts (#2171)
* fix(omp): limit thinking levels to model's reported efforts The omp provider exposed all six thinking levels (off/minimal/low/ medium/high/xhigh) whenever a model had reasoning enabled, ignoring the per-model thinking config that omp reports via RPC. The OmpModelSchema didn't parse the thinking field at all, so the model's efforts subset and defaultLevel were discarded. Parse model.thinking (efforts/defaultLevel/effortMap) in OmpModelSchema and filter the thinking options in mapOmpModel to only the model's reported efforts. The default is the reported defaultLevel when it's in the filtered set, otherwise the first (lowest) effort. Older omp versions that don't report thinking.efforts keep getting the full set. Also fix createSession to not hardcode 'medium' as the thinking fallback when the client doesn't send a thinkingOptionId — pass undefined so omp uses its own model default instead of overriding it with a level that may not even be in the model's effort set. * test(omp): drop --thinking medium from provider-registry launch assertions The thinking-level filtering PR changed createSession to pass undefined instead of "medium" when no thinking option is selected, so buildOmpLaunch no longer emits --thinking. agent.test.ts was updated but provider-registry.test.ts still expected --thinking medium in two OMP launch assertions, failing server-tests on ubuntu and windows. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
@@ -541,7 +541,7 @@ test("OMP is a disabled built-in backed by the real OMP adapter", async () => {
|
||||
expect.objectContaining({
|
||||
cwd: "/tmp/registry-omp",
|
||||
protocolMode: "rpc-ui",
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "yolo", "--thinking", "medium"],
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "yolo"],
|
||||
}),
|
||||
]);
|
||||
await session.close();
|
||||
@@ -598,8 +598,6 @@ test("built-in OMP override keeps the real OMP adapter enabled and launchable",
|
||||
"rpc-ui",
|
||||
"--approval-mode",
|
||||
"yolo",
|
||||
"--thinking",
|
||||
"medium",
|
||||
]);
|
||||
await session.close();
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("OMP agent client and session", () => {
|
||||
cwd: "/tmp/paseo-omp-agent-test",
|
||||
protocolMode: "rpc-ui",
|
||||
modeId: "ask",
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "always-ask", "--thinking", "medium"],
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "always-ask"],
|
||||
});
|
||||
expect(omp.registeredHostTools()).toEqual([
|
||||
[expect.objectContaining({ name: "create_agent" })],
|
||||
@@ -117,10 +117,25 @@ describe("OMP agent client and session", () => {
|
||||
cwd: "/tmp/paseo-omp-agent-test",
|
||||
protocolMode: "rpc-ui",
|
||||
modeId: "write",
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "write", "--thinking", "medium"],
|
||||
argv: ["omp", "--mode", "rpc-ui", "--approval-mode", "write"],
|
||||
});
|
||||
});
|
||||
|
||||
test("passes --thinking when a thinking option is provided", async () => {
|
||||
const omp = new OmpHarness();
|
||||
await omp.start({ modeId: "ask", thinkingOptionId: "xhigh" }, createToolCatalog());
|
||||
|
||||
expect(omp.launchConfiguration().argv).toEqual([
|
||||
"omp",
|
||||
"--mode",
|
||||
"rpc-ui",
|
||||
"--approval-mode",
|
||||
"always-ask",
|
||||
"--thinking",
|
||||
"xhigh",
|
||||
]);
|
||||
});
|
||||
|
||||
test("streams a prompt through completion", async () => {
|
||||
const omp = new OmpHarness();
|
||||
await omp.start();
|
||||
|
||||
@@ -107,9 +107,9 @@ import {
|
||||
buildOmpRpcUiPermissionResponse,
|
||||
mapOmpRpcUiPermissionRequest,
|
||||
} from "./rpc-ui-permission-mapper.js";
|
||||
import { DEFAULT_OMP_THINKING_LEVEL, mapOmpModel } from "./map-omp-model.js";
|
||||
|
||||
const OMP_PROVIDER = "omp";
|
||||
const DEFAULT_OMP_THINKING_LEVEL: OmpThinkingLevel = "medium";
|
||||
const OMP_CATALOG_REQUEST_TIMEOUT_MS = 120_000;
|
||||
const QUESTION_RESPONSE_HEADER = "Response";
|
||||
const QUESTION_COMMENT_HEADER = "Comment";
|
||||
@@ -129,21 +129,6 @@ const OMP_CORE_CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsRewindBoth: false,
|
||||
};
|
||||
|
||||
const OMP_THINKING_OPTIONS: ReadonlyArray<{
|
||||
id: OmpThinkingLevel;
|
||||
label: string;
|
||||
description: string;
|
||||
isDefault?: boolean;
|
||||
}> = [
|
||||
{ id: "off", label: "Off", description: "No extra reasoning" },
|
||||
{ id: "minimal", label: "Minimal", description: "Light reasoning" },
|
||||
{ id: "low", label: "Low", description: "Faster reasoning" },
|
||||
{ id: "medium", label: "Medium", description: "Balanced reasoning", isDefault: true },
|
||||
{ id: "high", label: "High", description: "Deeper reasoning" },
|
||||
{ id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
|
||||
{ id: "max", label: "Max", description: "Maximum reasoning" },
|
||||
] as const;
|
||||
|
||||
export interface OmpAgentClientOptions {
|
||||
logger: Logger;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
@@ -320,21 +305,6 @@ function parseAutoCompactMode(value: string | undefined): AutoCompactMode {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function mapThinkingOption(option: (typeof OMP_THINKING_OPTIONS)[number]) {
|
||||
const mappedOption = {
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
};
|
||||
if (option.isDefault) {
|
||||
return {
|
||||
...mappedOption,
|
||||
isDefault: true,
|
||||
};
|
||||
}
|
||||
return mappedOption;
|
||||
}
|
||||
|
||||
function toAgentUsage(stats: OmpSessionStats): AgentUsage | undefined {
|
||||
const inputTokens = stats.tokens?.input ?? 0;
|
||||
const cachedInputTokens = stats.tokens?.cacheRead ?? 0;
|
||||
@@ -883,21 +853,6 @@ function buildExtensionUiResponse(
|
||||
return { value: answer };
|
||||
}
|
||||
|
||||
function mapOmpModel(model: OmpModel, provider: AgentProvider): AgentModelDefinition {
|
||||
return {
|
||||
provider,
|
||||
id: `${model.provider}/${model.id}`,
|
||||
label: `${model.provider}/${model.name ?? model.id}`,
|
||||
description: `${model.provider}/${model.id}`,
|
||||
metadata: {
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
},
|
||||
thinkingOptions: model.reasoning ? OMP_THINKING_OPTIONS.map(mapThinkingOption) : undefined,
|
||||
defaultThinkingOptionId: model.reasoning ? DEFAULT_OMP_THINKING_LEVEL : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntime(
|
||||
logger: Logger,
|
||||
runtimeSettings: ProviderRuntimeSettings | undefined,
|
||||
@@ -2301,8 +2256,7 @@ export class OmpAgentClient implements AgentClient {
|
||||
cwd: config.cwd,
|
||||
protocolMode: "rpc-ui",
|
||||
model: config.model,
|
||||
thinkingOptionId:
|
||||
normalizeOmpThinkingOption(config.thinkingOptionId) ?? DEFAULT_OMP_THINKING_LEVEL,
|
||||
thinkingOptionId: normalizeOmpThinkingOption(config.thinkingOptionId) ?? undefined,
|
||||
noSession: config.internal === true,
|
||||
modeId: launchMode.modeId,
|
||||
extraArgs: launchMode.extraArgs,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { mapOmpModel } from "./map-omp-model.js";
|
||||
import type { OmpModel } from "./rpc-types.js";
|
||||
|
||||
function baseModel(overrides: Partial<OmpModel> = {}): OmpModel {
|
||||
return {
|
||||
provider: "pioneer",
|
||||
id: "canada-quant/glm-5.2",
|
||||
name: "GLM-5.2",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("mapOmpModel thinking options", () => {
|
||||
test("limits thinking options to the model's reported efforts", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "effort",
|
||||
efforts: ["high", "xhigh"],
|
||||
defaultLevel: "xhigh",
|
||||
effortMap: { high: "high", xhigh: "max" },
|
||||
},
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual(["high", "xhigh"]);
|
||||
expect(result.defaultThinkingOptionId).toBe("xhigh");
|
||||
expect(result.thinkingOptions?.find((option) => option.isDefault)?.id).toBe("xhigh");
|
||||
});
|
||||
|
||||
test("exposes the full set when reasoning is true but no thinking config is reported", () => {
|
||||
const model = baseModel({ reasoning: true });
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
]);
|
||||
expect(result.defaultThinkingOptionId).toBe("medium");
|
||||
});
|
||||
|
||||
test("exposes the full set when efforts is empty", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: { mode: "effort", efforts: [], defaultLevel: "high" },
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
]);
|
||||
});
|
||||
|
||||
test("omits thinking options entirely when reasoning is false", () => {
|
||||
const model = baseModel({ reasoning: false });
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions).toBeUndefined();
|
||||
expect(result.defaultThinkingOptionId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("omits thinking options when reasoning is absent", () => {
|
||||
const model = baseModel({});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions).toBeUndefined();
|
||||
expect(result.defaultThinkingOptionId).toBeUndefined();
|
||||
});
|
||||
|
||||
test("falls back to the first available option when defaultLevel is not in efforts", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: { mode: "effort", efforts: ["low", "high"], defaultLevel: "xhigh" },
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual(["low", "high"]);
|
||||
expect(result.defaultThinkingOptionId).toBe("low");
|
||||
expect(result.thinkingOptions?.find((option) => option.isDefault)?.id).toBe("low");
|
||||
});
|
||||
|
||||
test("uses the first option as default when defaultLevel is absent", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: { mode: "effort", efforts: ["low", "medium", "high"] },
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual(["low", "medium", "high"]);
|
||||
expect(result.defaultThinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
test("falls back to the full set when every reported effort is unknown", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: { mode: "effort", efforts: ["ultra", "turbo"], defaultLevel: "turbo" },
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
]);
|
||||
expect(result.defaultThinkingOptionId).toBe("medium");
|
||||
});
|
||||
|
||||
test("preserves provider and model id in the mapped definition", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
thinking: { mode: "effort", efforts: ["high", "xhigh"], defaultLevel: "xhigh" },
|
||||
});
|
||||
|
||||
const result = mapOmpModel(model, "omp");
|
||||
|
||||
expect(result.provider).toBe("omp");
|
||||
expect(result.id).toBe("pioneer/canada-quant/glm-5.2");
|
||||
expect(result.label).toBe("pioneer/GLM-5.2");
|
||||
expect(result.metadata).toEqual({ provider: "pioneer", modelId: "canada-quant/glm-5.2" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
AgentProvider,
|
||||
AgentSelectOption,
|
||||
} from "../../agent-sdk-types.js";
|
||||
import type { OmpModel, OmpThinkingLevel } from "./rpc-types.js";
|
||||
|
||||
export const DEFAULT_OMP_THINKING_LEVEL: OmpThinkingLevel = "medium";
|
||||
|
||||
export const OMP_THINKING_OPTIONS: ReadonlyArray<{
|
||||
id: OmpThinkingLevel;
|
||||
label: string;
|
||||
description: string;
|
||||
isDefault?: boolean;
|
||||
}> = [
|
||||
{ id: "off", label: "Off", description: "No extra reasoning" },
|
||||
{ id: "minimal", label: "Minimal", description: "Light reasoning" },
|
||||
{ id: "low", label: "Low", description: "Faster reasoning" },
|
||||
{ id: "medium", label: "Medium", description: "Balanced reasoning", isDefault: true },
|
||||
{ id: "high", label: "High", description: "Deeper reasoning" },
|
||||
{ id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
|
||||
{ id: "max", label: "Max", description: "Maximum reasoning" },
|
||||
] as const;
|
||||
|
||||
function mapThinkingOption(
|
||||
option: (typeof OMP_THINKING_OPTIONS)[number],
|
||||
isDefault?: boolean,
|
||||
): AgentSelectOption {
|
||||
const mapped: AgentSelectOption = {
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
};
|
||||
if (isDefault ?? option.isDefault) {
|
||||
mapped.isDefault = true;
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export function mapOmpModel(model: OmpModel, provider: AgentProvider): AgentModelDefinition {
|
||||
const { thinkingOptions, defaultThinkingOptionId } = resolveOmpThinkingConfig(model);
|
||||
return {
|
||||
provider,
|
||||
id: `${model.provider}/${model.id}`,
|
||||
label: `${model.provider}/${model.name ?? model.id}`,
|
||||
description: `${model.provider}/${model.id}`,
|
||||
metadata: {
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
},
|
||||
thinkingOptions,
|
||||
defaultThinkingOptionId,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOmpThinkingConfig(model: OmpModel): {
|
||||
thinkingOptions: AgentSelectOption[] | undefined;
|
||||
defaultThinkingOptionId: string | undefined;
|
||||
} {
|
||||
if (!model.reasoning) {
|
||||
return { thinkingOptions: undefined, defaultThinkingOptionId: undefined };
|
||||
}
|
||||
const efforts = model.thinking?.efforts;
|
||||
if (!efforts || efforts.length === 0) {
|
||||
// Older omp versions don't report per-model thinking config; expose the full set.
|
||||
return {
|
||||
thinkingOptions: OMP_THINKING_OPTIONS.map((option) => mapThinkingOption(option)),
|
||||
defaultThinkingOptionId: DEFAULT_OMP_THINKING_LEVEL,
|
||||
};
|
||||
}
|
||||
const effortSet = new Set(efforts);
|
||||
const filtered = OMP_THINKING_OPTIONS.filter((option) => effortSet.has(option.id));
|
||||
if (filtered.length === 0) {
|
||||
// All reported efforts are unrecognized; fall back to the full set with the standard default.
|
||||
return {
|
||||
thinkingOptions: OMP_THINKING_OPTIONS.map((option) => mapThinkingOption(option)),
|
||||
defaultThinkingOptionId: DEFAULT_OMP_THINKING_LEVEL,
|
||||
};
|
||||
}
|
||||
const reportedDefault = model.thinking?.defaultLevel;
|
||||
const defaultThinkingOptionId =
|
||||
reportedDefault && filtered.some((option) => option.id === reportedDefault)
|
||||
? reportedDefault
|
||||
: (filtered[0]?.id ?? DEFAULT_OMP_THINKING_LEVEL);
|
||||
return {
|
||||
thinkingOptions: filtered.map((option) =>
|
||||
mapThinkingOption(option, option.id === defaultThinkingOptionId),
|
||||
),
|
||||
defaultThinkingOptionId,
|
||||
};
|
||||
}
|
||||
@@ -87,12 +87,22 @@ export const OmpAgentMessageSchema = z.discriminatedUnion("role", [
|
||||
OmpBashExecutionMessageSchema,
|
||||
]);
|
||||
|
||||
export const OmpModelThinkingSchema = z
|
||||
.object({
|
||||
mode: z.string().optional(),
|
||||
efforts: z.array(z.string()).optional(),
|
||||
defaultLevel: z.string().optional(),
|
||||
effortMap: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const OmpModelSchema = z
|
||||
.object({
|
||||
provider: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
reasoning: z.boolean().optional(),
|
||||
thinking: OmpModelThinkingSchema.optional(),
|
||||
contextWindow: z.number().optional(),
|
||||
maxTokens: z.number().nullable().optional(),
|
||||
api: z.string().optional(),
|
||||
@@ -568,6 +578,7 @@ export type OmpToolCallContent = z.infer<typeof OmpToolCallContentSchema>;
|
||||
export type OmpAssistantContent = z.infer<typeof OmpAssistantContentSchema>;
|
||||
export type OmpAgentMessage = z.infer<typeof OmpAgentMessageSchema>;
|
||||
export type OmpModel = z.infer<typeof OmpModelSchema>;
|
||||
export type OmpModelThinking = z.infer<typeof OmpModelThinkingSchema>;
|
||||
export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
|
||||
export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
|
||||
export type OmpRpcSlashCommand = z.infer<typeof OmpRpcSlashCommandSchema>;
|
||||
|
||||
Reference in New Issue
Block a user