Skip to content
Merged
11 changes: 10 additions & 1 deletion apps/desktop/src/lib/composer-models.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
bindingSupportsImages,
effectiveContextWindow,
isImageGenerationModel,
type ImageGenerationBindings,
modelMatchesFilter,
Expand Down Expand Up @@ -43,13 +44,21 @@ export function composerModelsForProvider(
const metadata = (discovered ?? []).find((model) =>
sameComposerModelId(model.modelId, modelId),
);
const binding = composerModelBinding(provider, modelId);
const contextWindow = effectiveContextWindow(
metadata?.contextWindow,
binding?.contextWindow,
binding?.contextWindowSource,
);
const contextLimit = contextWindow === undefined ? {} : { contextWindow };
const displayName = modelId;
return metadata
? { ...metadata, modelId, displayName, providerId: provider.id }
? { ...metadata, ...contextLimit, modelId, displayName, providerId: provider.id }
: {
modelId,
displayName,
providerId: provider.id,
...contextLimit,
capabilities: ["text"],
source: "user" as const,
};
Expand Down
50 changes: 50 additions & 0 deletions apps/desktop/test/composer-models.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,56 @@ test("configured models remain selectable when discovery is unavailable", () =>
assert.equal(models[0].displayName, "my-model-v2");
});

test("Composer context labels use configured limits and retain them without discovery", () => {
const provider = {
id: "space-bunny",
models: [
{
...binding("claude-opus-5-5"),
contextWindow: 500_000,
contextWindowSource: "user",
},
],
};
const [discovered] = composerModelsForProvider(provider, [
{ ...model("claude-opus-5-5"), contextWindow: 1_000_000 },
]);

assert.equal(discovered.contextWindow, 500_000);

const [catalogOwned] = composerModelsForProvider(
{
id: "space-bunny",
models: [
{
...binding("catalog-model"),
contextWindow: 128_000,
contextWindowSource: "catalog",
},
],
},
[{ ...model("catalog-model"), contextWindow: 1_000_000 }],
);

assert.equal(catalogOwned.contextWindow, 1_000_000);

const [undiscovered] = composerModelsForProvider(
{
id: "space-bunny",
models: [
{
...binding("custom-model"),
contextWindow: 333_333,
contextWindowSource: "user",
},
],
},
undefined,
);

assert.equal(undiscovered.contextWindow, 333_333);
});

test("unmatched models expose the full thinking ladder unless a binding overrides it", () => {
const provider = {
id: "custom",
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/03-runtime/13-model-catalog-and-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ retain the existing session configuration and permission boundaries.

Warnings are non-blocking unless execution is impossible.

Composer context-window labels resolve the exact binding with `effectiveContextWindow`: explicit user limits override discovery, catalog-owned limits follow discovery, and legacy bindings preserve their saved limits. A binding with a valid saved limit still displays it when discovery is unavailable. This display resolution does not modify the discovered catalog or runtime safety budgets.

### 11.1 Reasoning capability resolution

1. Resolve published Pi thinking metadata for the exact physical model.
Expand Down
6 changes: 6 additions & 0 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -17117,3 +17117,9 @@ host-created files. The full app's file-preview viewer is covered separately.
Playwright can be supplied through `PI_TEST_PLAYWRIGHT`). Only the model server
is simulated in the Electron flow. The fixture profile and screenshots stay
under `.artifacts/` for inspection; no user profile or paid model is used.

### E2E-COMPOSER-configured-context-window

- **Steps:** Save a 500K user context limit for a model whose catalog publishes 1M. Open the Composer model list, then send a short message and inspect context usage. Repeat with a catalog-owned limit and with discovery unavailable.
- **Expected:** The user-configured row and context inspector show 500K; a catalog-owned row follows the published limit; a configured model without discovery retains its saved context label. Model selection remains unchanged.
- **Status:** Automated unit regression in `apps/desktop/test/composer-models.test.mjs` covers user overrides, catalog inheritance, and missing discovery; the full save → picker → message → context-inspector path remains a manual validation scenario.
2 changes: 2 additions & 0 deletions docs/zh-CN/spec/03-runtime/13-model-catalog-and-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ type ModelCatalogItem = {

除非不可能执行,否则警告是非阻塞的。

Composer 模型列表中的上下文窗口标签通过 `effectiveContextWindow` 按对应 binding 解析:显式的用户额度优先于发现信息;由目录管理的额度跟随发现结果;旧版 binding 保留已保存额度。即使模型发现不可用,只要已保存额度有效,也仍会显示该额度。该显示解析不会修改发现目录或运行时安全预算。

### 11.1 Reasoning capability resolution

1. Resolve published Pi thinking metadata for the exact physical model.
Expand Down
6 changes: 6 additions & 0 deletions docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -9449,3 +9449,9 @@ preload, API normalization, store events, ToolRow, and Markdown renderer.
No provider credentials or paid model calls are required. The fixture ends at
work-panel file-request routing; artifact bytes are verified from the real
host-created files. The full app's file-preview viewer is covered separately.

### E2E-COMPOSER-configured-context-window

- **步骤:**为目录发布 1M 的模型保存 500K 用户额度。打开 Composer 模型列表,然后发送一条简短消息并检查上下文用量。对目录管理的额度以及模型发现不可用的情况重复验证。
- **预期:**用户配置的模型行和上下文检查器显示 500K;目录管理的额度跟随已发布值;没有发现信息的已配置模型仍显示保存的额度。模型选择保持不变。
- **状态:**`apps/desktop/test/composer-models.test.mjs` 自动覆盖用户覆盖值、目录继承值及发现不可用时保留额度;完整的“保存 → 模型列表 → 发送消息 → 上下文检查器”路径仍需手动验证。
Loading