Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
21 changes: 18 additions & 3 deletions docs-site/src/content/docs/guides/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,24 @@ small-context model is never given more output than context. It is not a claim a
model's true maximum.

Two fields are deliberately absent. `cost` requires all four price fields and opencodex has no
price data for routed models — emitting zeros would assert that every model is free. `reasoning` is
a boolean in Pi while the catalog carries an effort ladder, and mapping one onto the other would be
a guess.
price data for routed models — emitting zeros would assert that every model is free.

`reasoning` is the one field that used to be absent and now is not: Pi stores a boolean while the
catalog carries an effort ladder, and mapping one onto the other used to be a guess. Since the
catalog's ladder is the proxy's own statement about whether a model accepts reasoning parameters
(adapters honor `reasoning_effort`), an export row with a **non-empty** ladder now emits
`"reasoning": true`, and a row without one (or with an explicitly empty ladder) stays
reasoning-free. Pi then offers its effort control for exactly the models opencodex will accept it
on. The export also emits a `thinkingLevelMap` that hides every pi level outside the declared
ladder (`null`), so pi never offers — and never sends — an effort the ladder does not contain.
If you need a different mapping, hand-edit `thinkingLevelMap` afterward as documented by Pi.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Treat `reasoning` as Pi-UI metadata: it is derived from the catalog ladder, not proof that the
upstream natively supports a reasoning parameter. What the proxy actually sends for a given
`reasoning_effort` value depends on the provider's adapter and model — it may pass the value
through, translate it (wire aliases), clamp it to the configured ladder, emulate it, or omit it
entirely (e.g. `noReasoningModels`). The boolean only controls whether Pi offers the control at
all.

## Schema status

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ export const de: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "z. B. Qwen 4 Max Preview",
"models.customFieldContext": "Kontextfenster",
"models.customFieldModalities": "Eingabemodalitäten",
"models.customFieldReasoning": "Reasoning-Aufwand",
"models.customFieldReasoningOverride": "Reasoning-Aufwand überschreiben",
"models.tipProvider": "Anbieter",
"models.tipContext": "Kontext",
"models.tipModalities": "Modalitäten",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ export const en = {
"models.customFieldDisplayNamePlaceholder": "e.g. Qwen 4 Max Preview",
"models.customFieldContext": "Context window",
"models.customFieldModalities": "Input modalities",
"models.customFieldReasoning": "Reasoning effort",
"models.customFieldReasoningOverride": "Override reasoning effort",
"models.tipProvider": "Provider",
"models.tipContext": "Context",
"models.tipModalities": "Modalities",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,8 @@ export const ja: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "e.g. Qwen 4 Max Preview",
"models.customFieldContext": "Context window",
"models.customFieldModalities": "Input modalities",
"models.customFieldReasoning": "推論努力",
"models.customFieldReasoningOverride": "推論努力を上書き",
"models.tipProvider": "Provider",
"models.tipContext": "Context",
"models.tipModalities": "Modalities",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ export const ko: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "예: Qwen 4 Max Preview",
"models.customFieldContext": "컨텍스트 윈도우",
"models.customFieldModalities": "입력 모달리티",
"models.customFieldReasoning": "추론 노력",
"models.customFieldReasoningOverride": "추론 노력 재정의",
"models.tipProvider": "프로바이더",
"models.tipContext": "컨텍스트",
"models.tipModalities": "모달리티",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,8 @@ export const ru: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "например, Qwen 4 Max Preview",
"models.customFieldContext": "Контекстное окно",
"models.customFieldModalities": "Входные модальности",
"models.customFieldReasoning": "Уровень рассуждений",
"models.customFieldReasoningOverride": "Переопределить уровень рассуждений",
"models.tipProvider": "Провайдер",
"models.tipContext": "Контекст",
"models.tipModalities": "Модальности",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,8 @@ export const tr: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "örn. Qwen 4 Max Preview",
"models.customFieldContext": "Bağlam penceresi",
"models.customFieldModalities": "Girdi türleri",
"models.customFieldReasoning": "Akıl yürütme çabası",
"models.customFieldReasoningOverride": "Akıl yürütme çabasını geçersiz kıl",
"models.tipProvider": "Sağlayıcı",
"models.tipContext": "Bağlam",
"models.tipModalities": "Girdi Türleri",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ export const zhTW: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "例如 Qwen 4 Max Preview",
"models.customFieldContext": "上下文視窗",
"models.customFieldModalities": "輸入模態",
"models.customFieldReasoning": "推理強度",
"models.customFieldReasoningOverride": "覆寫推理強度",
"models.tipProvider": "供應商",
"models.tipContext": "上下文",
"models.tipModalities": "模態",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,8 @@ export const zh: Record<TKey, string> = {
"models.customFieldDisplayNamePlaceholder": "例如 Qwen 4 Max Preview",
"models.customFieldContext": "上下文窗口",
"models.customFieldModalities": "输入模态",
"models.customFieldReasoning": "推理强度",
"models.customFieldReasoningOverride": "覆盖推理强度",
"models.tipProvider": "提供方",
"models.tipContext": "上下文",
"models.tipModalities": "模态",
Expand Down
69 changes: 68 additions & 1 deletion gui/src/pages/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
THREAD_OPTIONS,
writeCollapsedProviders,
discoveryFailureLabel,
REASONING_EFFORT_LEVELS,
type ModelRow,
type ProviderContextCapsResponse,
type ShadowCallData,
Expand Down Expand Up @@ -193,6 +194,11 @@ export default function Models({ apiBase }: { apiBase: string }) {
const [customFormContextWindow, setCustomFormContextWindow] = useState("");
const [customFormShowCustomCtx, setCustomFormShowCustomCtx] = useState(false);
const [customFormModalities, setCustomFormModalities] = useState<string[]>(["text"]);
const [customFormReasoning, setCustomFormReasoning] = useState(false);
const [customFormReasoningEfforts, setCustomFormReasoningEfforts] = useState<string[]>([]);
// The ladder loaded from the row being edited. Re-enabling the override must restore this
// instead of re-pre-checking every level, which would silently discard a stored ladder.
const customFormReasoningLoadedRef = useRef<string[]>([]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const [customSaving, setCustomSaving] = useState(false);
const [customError, setCustomError] = useState("");
const [contextModalProvider, setContextModalProvider] = useState<string | null>(null);
Expand Down Expand Up @@ -831,14 +837,15 @@ export default function Models({ apiBase }: { apiBase: string }) {
displayName?: string,
contextWindow?: number,
inputModalities?: string[],
reasoningEfforts?: string[],
) => {
setCustomSaving(true);
setCustomError("");
try {
const r = await fetch(`${apiBase}/api/custom-models`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, modelId, displayName, contextWindow, inputModalities }),
body: JSON.stringify({ provider, modelId, displayName, contextWindow, inputModalities, reasoningEfforts }),
});
try {
await readJsonOrThrow(r, t("models.customSaveFailed"));
Expand Down Expand Up @@ -1000,6 +1007,9 @@ export default function Models({ apiBase }: { apiBase: string }) {
setCustomFormContextWindow("");
setCustomFormShowCustomCtx(false);
setCustomFormModalities(["text"]);
setCustomFormReasoning(false);
setCustomFormReasoningEfforts([]);
customFormReasoningLoadedRef.current = [];
setCustomError("");
setCustomModalOpen(true);
}}
Expand Down Expand Up @@ -1146,6 +1156,12 @@ export default function Models({ apiBase }: { apiBase: string }) {
setCustomFormContextWindow(m.contextWindow ? String(m.contextWindow) : "");
setCustomFormShowCustomCtx(false);
setCustomFormModalities(m.inputModalities ?? ["text"]);
// Only a STORED ladder counts as "configured": an inherited one
// would show a phantom override that saves "inherit" over the
// provider row's current metadata.
setCustomFormReasoning(Array.isArray(m.reasoningEfforts));
setCustomFormReasoningEfforts(m.reasoningEfforts ?? []);
customFormReasoningLoadedRef.current = m.reasoningEfforts ?? [];
setCustomError("");
setCustomModalOpen(true);
setHoveredModel(null);
Expand Down Expand Up @@ -1595,6 +1611,52 @@ export default function Models({ apiBase }: { apiBase: string }) {
))}
</div>
</div>

<div className="text-label models-field">
{t("models.customFieldReasoning")}
<div className="row models-field-row">
<label className="row models-modality-option">
<input
type="checkbox"
checked={customFormReasoning}
onChange={e => {
setCustomFormReasoning(e.target.checked);
if (e.target.checked) {
setCustomFormReasoningEfforts(prev => {
// First enable (nothing picked yet): seed from the stored ladder,
// or pre-check the full set for a new row / an explicit
// no-reasoning row. Re-enable keeps whatever the user had.
if (prev.length > 0) return prev;
const loaded = customFormReasoningLoadedRef.current;
return loaded.length > 0 ? loaded : [...REASONING_EFFORT_LEVELS];
});
Comment thread
Wibias marked this conversation as resolved.
Outdated
}
}}
disabled={customSaving}
/>
<span className="text-control">{t("models.customFieldReasoningOverride")}</span>
</label>
</div>
{customFormReasoning && (
<div className="row models-field-row" style={{ flexWrap: "wrap" }}>
{REASONING_EFFORT_LEVELS.map(effort => (
<label key={effort} className="row models-modality-option">
<input
type="checkbox"
checked={customFormReasoningEfforts.includes(effort)}
onChange={e => {
setCustomFormReasoningEfforts(prev => (
e.target.checked ? [...prev, effort] : prev.filter(level => level !== effort)
));
}}
disabled={customSaving}
/>
<span className="text-control">{effort}</span>
</label>
))}
</div>
)}
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>

<div className="modal-actions">
Expand All @@ -1611,19 +1673,24 @@ export default function Models({ apiBase }: { apiBase: string }) {
const ctxVal = customFormContextWindow ? Number(customFormContextWindow.replace(/[_,\s]/g, "")) : undefined;
const contextWindow = ctxVal && ctxVal > 0 ? Math.floor(ctxVal) : undefined;
if (customModalMode === "add") {
const reasoningEfforts = customFormReasoning ? customFormReasoningEfforts : undefined;
void addCustomModel(
customModalProvider,
modelId,
displayName || undefined,
contextWindow,
customFormModalities.length > 0 ? customFormModalities : undefined,
reasoningEfforts,
);
} else {
// `null` clears a stored override back to "inherit from the provider row";
// an explicit empty ladder stays stored as "no reasoning".
void updateCustomModel(customModalId, {
modelId,
displayName,
contextWindow: contextWindow ?? null,
inputModalities: customFormModalities,
reasoningEfforts: customFormReasoning ? customFormReasoningEfforts : null,
});
}
}}
Expand Down
11 changes: 11 additions & 0 deletions gui/src/pages/models-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ export interface ModelRow {
contextWindow?: number;
contextCap?: number;
contextCapped?: boolean;
/** Stored custom-row override (not the inherited ladder); only present on custom rows. */
reasoningEfforts?: string[];
}

/**
* Reasoning-effort labels offered in the custom-model dialog. The full set of real
* `reasoning_effort` values (none, minimal, low, medium, high, xhigh, max). Deliberately
* excludes `ultra`: that is a Codex catalog label for the multi-agent collab surface, not a
* real `reasoning_effort` value — codex-rs converts it to `max` before any provider
* request, and the catalog writer appends it to every non-empty ladder anyway.
*/
export const REASONING_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;

export interface ProviderContextCapsResponse {
cap?: number;
value?: number;
Expand Down
18 changes: 18 additions & 0 deletions gui/src/styles-models-workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@
gap: var(--space-4);
}

.models-field-stack {
display: flex;
flex-direction: column;
gap: var(--space-4);
}

.models-field {
display: flex;
flex-direction: column;
Expand All @@ -382,6 +388,18 @@
cursor: pointer;
}

/* Uniform checkbox rendering in the custom-model dialog: native checkbox size is
browser-dependent (Chrome ~13px, Safari ~16px) and can even differ between rows in
some renderers, so pin an explicit size for every checkbox in the modal. 13px is the
size Chrome renders natively here; pinning it keeps the effort-step checkboxes exactly
as large as the other dialog checkboxes. */
.modal-card .models-modality-option input[type="checkbox"] {
width: 13px;
height: 13px;
margin: 0;
flex: none;
}

/* Beat .row { gap: 10px } (defined later in styles.css). */
.row.models-model-row,
.row.models-cap-row,
Expand Down
23 changes: 22 additions & 1 deletion src/cli/models-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
const USAGE = `Usage:
ocx models live [--provider <name>] [--json]
ocx models edit <custom-id> [--model-id <id>] [--display-name <name|->]
[--context-window <tokens|0>] [--modalities <text,image,audio|->] [--json]
[--context-window <tokens|0>] [--modalities <text,image,audio|->]
[--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra|->]
[--default-reasoning-effort <level|->] [--json]
ocx models <enable|disable> <provider/model|native-model> [--native] [--json]
ocx models provider <name> <on|off> [--json]
ocx models selected <provider> [--set <id,id...>|--clear] [--json]
Expand Down Expand Up @@ -57,6 +59,8 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const displayName = takeOption(args, "--display-name");
const contextRaw = takeOption(args, "--context-window");
const modalitiesRaw = takeOption(args, "--modalities");
const reasoningEffortsRaw = takeOption(args, "--reasoning-efforts");
const defaultEffortRaw = takeOption(args, "--default-reasoning-effort");
rejectArgs(args, USAGE);
if (modelId !== undefined) patch.modelId = modelId;
if (displayName !== undefined) patch.displayName = displayName === "-" ? "" : displayName;
Expand All @@ -66,6 +70,23 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
patch.contextWindow = value === 0 ? null : value;
}
if (modalitiesRaw !== undefined) patch.inputModalities = modalitiesRaw === "-" ? [] : csv(modalitiesRaw);
// "-" restores inheritance by clearing the stored ladder (null); "" stores an explicit
// empty ladder (the "no reasoning" override, same as the dashboard's uncheck-all).
// Embedded blank CSV members (`low,,high`, `,,`) are malformed and must be rejected, not
// silently normalized by csv().
if (reasoningEffortsRaw !== undefined) {
if (reasoningEffortsRaw === "-") {
patch.reasoningEfforts = null;
} else {
const trimmed = reasoningEffortsRaw.trim();
const values = trimmed === "" ? [] : trimmed.split(",").map(value => value.trim());
if (values.some(value => value === "")) {
throw new CliUsageError("--reasoning-efforts must be comma-separated values from none, minimal, low, medium, high, xhigh, max, ultra (\"\" for no reasoning, \"-\" to inherit)", USAGE);
}
patch.reasoningEfforts = values;
}
}
if (defaultEffortRaw !== undefined) patch.defaultReasoningEffort = defaultEffortRaw === "-" ? null : defaultEffortRaw;
if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE);
const result = await runtimeRequest(`/api/custom-models/${encodeURIComponent(id)}`, {
method: "PUT",
Expand Down
Loading
Loading