Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7f9ac16
feat(providers): support StepFun Step 5 Preview
zszz3 Sep 21, 2026
e0e4142
fix(providers): use StepFun Plan and preserve retry settings
zszz3 Sep 21, 2026
9f6372c
chore(integration): sync StepFun Plan with upstream main
zszz3 Sep 21, 2026
f280aa3
chore(integration): refresh StepFun Plan candidate
zszz3 Sep 21, 2026
db99a6f
chore(integration): resolve StepFun Plan conflicts with main
zszz3 Sep 21, 2026
a6dbf0e
chore(providers): remove newly added tests from StepFun Plan PR
zszz3 Sep 21, 2026
0ca8263
chore(integration): preserve StepFun and image generation docs
zszz3 Sep 21, 2026
0fdbc15
chore(integration): resolve conflicts with hosted search updates
zszz3 Sep 22, 2026
bc2785e
fix(providers): reconcile StepFun catalog with current main
zszz3 Sep 23, 2026
c73e129
fix(providers): preserve ambiguous catalog lookup guard
zszz3 Sep 24, 2026
139611c
chore(providers): sync StepFun branch with main
zszz3 Sep 24, 2026
893d11d
fix(providers): default StepFun Plan to Anthropic Messages
zszz3 Sep 24, 2026
4c3885a
chore(providers): refresh StepFun integration candidate
zszz3 Sep 24, 2026
64411c7
fix(i18n): keep prompt template labels syntactically valid
zszz3 Sep 24, 2026
c7b639e
fix(i18n): repair quoted template label properties
zszz3 Sep 24, 2026
c9a15fe
chore(providers): incorporate latest settings editor
zszz3 Sep 24, 2026
4345c9b
fix(providers): reconcile voice integration with latest main
zszz3 Sep 24, 2026
5a04e9e
docs(providers): simplify StepFun Plan transport guidance
zszz3 Sep 24, 2026
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
9 changes: 0 additions & 9 deletions apps/desktop/electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ import { registerWindowIpc } from "./ipc/window-ipc";
import { registerPullsIpc } from "./ipc/pulls-ipc";
import { registerAgentIpc } from "./ipc/agent-ipc";
import { registerIpcHandlers } from "./ipc/register";
import { VoiceService } from "./voice-service";
import {
type WindowLifecycleState,
} from "./bootstrap/window";
Expand Down Expand Up @@ -1247,13 +1246,6 @@ runtimeLifecycle = createRuntimeLifecycle({
});
const { bootHostStatus, runtimeArch, bootBackends } = runtimeLifecycle;

// Voice service — created lazily on first use, disposed on quit.
const voiceService = new VoiceService(
dataDir + "/voice-models",
() => mainWindow,
);
app.once("before-quit", () => voiceService.dispose());

function registerIpc() {
return registerIpcHandlers({
traySessions: applicationLifecycle!.traySessions,
Expand Down Expand Up @@ -1345,7 +1337,6 @@ function registerIpc() {
getPluginPanelTheme: () => pluginPanelTheme,
isDeveloperMode: () => developerMode,
sendToRenderer,
voiceService,
});
}

Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/electron/main/ipc/provider-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ export function registerProviderIpc({
// ModelInfo is catalog metadata. Keep its published reasoning fields
// intact; Composer and runtime resolve the exact user binding when
// they need effective per-provider capabilities.
...(modelsDevModel ? { catalogSource: "models.dev" as const } : {}),
...(modelsDevModel
? { catalogSource: modelsDevModel.metadataSource ?? "models.dev" as const }
: {}),
};
};

Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/electron/main/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { createComposerTemplateLoader, registerWorkspaceIpc } from "./workspace-
import { registerComposerIpc } from "./composer-ipc";
import { registerSpeechIpc } from "./speech-ipc";
import { registerVoiceIpc } from "./voice-ipc";
import { createVoiceService } from "../voice-service";
import type { IpcRegistrar } from "./types";
import type { createTraySessions } from "../tray-sessions";

Expand Down Expand Up @@ -154,7 +155,7 @@ export function registerIpcHandlers(dependencies: RegisterIpcDependencies) {
getPluginPanelTheme,
isDeveloperMode,
sendToRenderer,
voiceService,
voiceService = createVoiceService(dataDir, getMainWindow),
} = dependencies;


Expand Down
16 changes: 12 additions & 4 deletions apps/desktop/electron/main/models-dev-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile } from "node:fs/promises";
import { MODEL_VENDOR_PREFIXES, catalogModelIdsMatch, stripVariantSuffix } from "@pi-desktop/shared";
import { stepfunModelSupplement } from "./stepfun-model-metadata.ts";
import { MODEL_VENDOR_PREFIXES, catalogModelIdsMatch, modelIdsMatch, stripVariantSuffix } from "@pi-desktop/shared";
import type {
ModelCost,
ModelCostTier,
Expand Down Expand Up @@ -54,6 +55,8 @@ export type ModelsDevProvider = {
};

export type ModelsDevModel = {
/** Absent for models.dev; explicit for a reviewed first-party supplement. */
metadataSource?: "provider";
providerKey: string;
providerName: string;
providerApi?: string;
Expand Down Expand Up @@ -695,7 +698,7 @@ export function modelInfoFromModelsDev(
capabilities: capabilityList(model),
supportedThinkingLevels: [...model.thinkingLevels],
source: "discovered",
catalogSource: "models.dev",
catalogSource: model.metadataSource ?? "models.dev",
};
}

Expand All @@ -720,7 +723,7 @@ export function modelConfigFromModelsDev(
...(model.cost?.tiers ? { tiers: model.cost.tiers } : {}),
};
const config: ModelConfig = {
source: "models.dev",
source: model.metadataSource ?? "models.dev",
name: model.displayName,
baseUrl: baseUrl ?? model.providerApi ?? "",
reasoning,
Expand Down Expand Up @@ -1030,12 +1033,17 @@ export class ModelsDevCatalog {
candidates.sort((left, right) =>
right.score - left.score || left.model.modelId.length - right.model.modelId.length,
);
const supplement = stepfunModelSupplement(input);
const firstParty = supplement && preferred.find(({ model, provider }) =>
apiMatches(input.baseUrl, provider.api) && modelIdsMatch(model.modelId, requested),
)?.model;
// An unknown endpoint can use a unique catalog match (including supported
// proxy aliases), but scores are not proof of provider identity. If two
// providers publish the same ID, do not borrow either one's metadata.
const result = preferredProvider || candidates.length === 1
const catalogMatch = preferredProvider || candidates.length === 1
? candidates[0]?.model
: undefined;
const result = firstParty ?? supplement ?? catalogMatch;
// Cache the result (a miss included) so a repeated miss is also O(1) and
// cannot grow the candidate index with query-dependent keys.
this.lookupMemo.set(memoKey, result);
Expand Down
46 changes: 46 additions & 0 deletions apps/desktop/electron/main/stepfun-model-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ModelsDevModel } from "./models-dev-catalog";

/**
* First-party metadata while models.dev has no StepFun Step 5 record.
* https://platform.stepfun.com/docs/zh/guides/models/step-5-preview
* Verified 2026-09-21: /v1/models publishes max_input_tokens=1024000,
* enable_vision_input=true and reasoning_effort_support_list=low/medium/high.
* The documented 64k output ceiling is represented conservatively as 64000.
* Never infer gateway capabilities from a model name or vendor label alone.
*/
export function stepfunModelSupplement(input: {
baseUrl?: string;
modelId: string;
}): ModelsDevModel | undefined {
if (input.modelId !== "step-5-preview" || !input.baseUrl) return undefined;
let endpoint: URL;
try {
endpoint = new URL(input.baseUrl);
} catch {
return undefined;
}
if (
endpoint.origin !== "https://api.stepfun.com" ||
!["/v1", "/step_plan/v1"].includes(endpoint.pathname.replace(/\/+$/, "")) ||
endpoint.username || endpoint.password || endpoint.search || endpoint.hash
) return undefined;
return {
providerKey: "stepfun",
providerName: "StepFun",
providerApi: `${endpoint.origin}${endpoint.pathname.replace(/\/+$/, "")}`,
metadataSource: "provider",
modelId: "step-5-preview",
displayName: "Step 5 Preview",
reasoning: true,
reasoningPublished: true,
thinkingLevels: ["low", "medium", "high"],
reasoningOptions: [{ type: "effort", values: ["low", "medium", "high"] }],
modalities: { input: ["text", "image", "video"], output: ["text"] },
modalitiesPublished: true,
inputPublished: true,
outputPublished: true,
toolCall: true,
structuredOutput: true,
limit: { context: 1_024_000, input: 1_024_000, output: 64_000 },
};
}
10 changes: 9 additions & 1 deletion apps/desktop/electron/main/voice-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* means zero import overhead at startup.
*/

import { BrowserWindow } from "electron";
import { app, BrowserWindow } from "electron";
import { join } from "node:path";
import { PvRecorderBackend, checkMicrophonePermission, requestMicrophonePermission } from "./audio-backend";
import type {
AudioCaptureFactory,
Expand Down Expand Up @@ -171,3 +172,10 @@ export class VoiceService {
}
}
}

/** Create the IPC-owned service and release it when Electron quits. */
export function createVoiceService(dataDir: string, getWindow: () => BrowserWindow | null): VoiceService {
const service = new VoiceService(join(dataDir, "voice-models"), getWindow);
app.once("before-quit", () => service.dispose());
return service;
}
2 changes: 1 addition & 1 deletion apps/desktop/src/features/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ export function SettingsPage() {
)}

{tab === "voice" && settings && (
<VoiceSettingsSection t={t} />
<VoiceSettingsSection t={t} settings={settings} saveSettings={saveSettings} />
)}

{tab === "shortcuts" && settings && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { useCallback, useEffect, useState } from "react";
import type { TFunction } from "i18next";
import type { AppSettings } from "@pi-desktop/shared";
import type { AppSettings, VoiceInputSettings } from "@pi-desktop/shared";
import { cx } from "../../../components/ui";
import { SettingsRow, SettingsCard } from "../primitives";
import { voiceIpc } from "../../voice/voice-ipc";
Expand Down Expand Up @@ -70,9 +70,9 @@ export function VoiceSettingsSection({
}, []);

const save = useCallback(
(patch: Record<string, unknown>) => {
(patch: Partial<VoiceInputSettings>) => {
const next = { ...voice, ...patch };
void saveSettings({ voice: next as any });
void saveSettings({ voice: next });
void voiceIpc.updateSettings(next);
},
[voice, saveSettings],
Expand Down Expand Up @@ -158,7 +158,12 @@ export function VoiceSettingsSection({
className="settings-select"
value={voice.chineseVariant}
disabled={!voice.enabled}
onChange={(e) => save({ chineseVariant: e.target.value })}
onChange={(e) => {
const chineseVariant = e.target.value;
if (chineseVariant === "simplified" || chineseVariant === "traditional-taiwan" || chineseVariant === "traditional-hong-kong") {
save({ chineseVariant });
}
}}
>
<option value="simplified">{t("settings.voiceSimplified")}</option>
<option value="traditional-taiwan">{t("settings.voiceTraditionalTaiwan")}</option>
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/styles/voice.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border-radius: 8px;
border-radius: var(--radius-xs);
background: var(--color-surface-elevated, #1e1e2e);
border: 1px solid var(--color-border, #333);
font-size: 13px;
font-size: var(--text-md);
gap: 12px;
}

Expand All @@ -51,7 +51,7 @@
.voice-recording-dot {
width: 8px;
height: 8px;
border-radius: 50%;
border-radius: var(--radius-round);
background: var(--color-danger, #ef4444);
animation: voice-pulse 1.2s ease-in-out infinite;
}
Expand All @@ -66,7 +66,7 @@
height: 14px;
border: 2px solid var(--color-border, #555);
border-top-color: var(--color-primary, #3b82f6);
border-radius: 50%;
border-radius: var(--radius-round);
animation: voice-spin 0.8s linear infinite;
}

Expand All @@ -77,22 +77,22 @@
.voice-volume-bar {
width: 80px;
height: 4px;
border-radius: 2px;
border-radius: var(--radius-full);
background: var(--color-border, #333);
overflow: hidden;
}

.voice-volume-fill {
height: 100%;
background: var(--color-primary, #3b82f6);
border-radius: 2px;
border-radius: var(--radius-full);
transition: width 100ms ease-out;
}

.voice-cancel-btn {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
border-radius: var(--radius-3xs);
font-size: var(--text-sm);
background: transparent;
color: var(--color-text-secondary, #999);
border: 1px solid var(--color-border, #444);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/test/packaging-footprint.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ test("macOS DMG is a two-icon install; ZIP keeps the unsigned helper", () => {

test("packaging does not include removed PTY native payload configuration", () => {
assert.deepEqual(packageJson.build.asar, { smartUnpack: false });
assert.equal(packageJson.build.asarUnpack, undefined);
assert.doesNotMatch(JSON.stringify(packageJson.build.asarUnpack ?? []), /node-pty/);
assert.doesNotMatch(JSON.stringify(packageJson.build.files), /node-pty/);
assert.doesNotMatch(JSON.stringify(packageJson.build.extraResources), /node-pty/);
});
1 change: 1 addition & 0 deletions apps/desktop/test/session-message-presentation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const shared = {
FileRefChip: () => null,
LinkifiedText: ({ text }) => text,
MessageAttachmentImage: () => null,
MessageTimestamp: () => null,
};

function loadComponent(name, extras = {}) {
Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0134-models-dev-sole-model-metadata-source.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR 0134: Use models.dev as the sole model metadata source with a local snapshot

- Status: Accepted
- Status: Accepted; amended by [StepFun first-party metadata](stepfun-first-party-model-metadata.md)
- Date: 2026-08-29
- Deciders: PI-Desktop core
- Amends: ADR 0027, ADR 0133, D136, D266
Expand All @@ -22,7 +22,9 @@ cost tiers. Those fields need one stable owner and a local offline snapshot.

## Decision

`https://models.dev/api.json` is the only model metadata/configuration source.
`https://models.dev/api.json` is the general model metadata/configuration source.
The StepFun amendment defines a source-labelled, exact-endpoint exception for
`step-5-preview` until its first-party record is available.

1. Electron main reads the bundled public document from
`resources/models.dev/api.json` in development and
Expand Down
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,6 @@ Each ADR includes:
| provider-display-order | [Provider display order](provider-display-order.md) | Accepted |
| registry-header-variable-spelling | [Remote header variables accept the registry's `{name}` spelling](registry-header-variable-spelling.md) | Proposed |
| provider-system-certificates | [Desktop sidecar uses OS-trusted certificates](provider-system-certificates.md) | Accepted |
| stepfun-first-party-model-metadata | [First-party StepFun metadata for a newly released model](stepfun-first-party-model-metadata.md) | Accepted (amends ADR 0134) |

| image-generation-capability | [Image generation as a configured Agent capability](image-generation-capability.md) | Accepted |
62 changes: 62 additions & 0 deletions docs/adr/stepfun-first-party-model-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADR: First-party StepFun metadata for a newly released model

- Status: Accepted
- Date: 2026-09-21
- Amends: ADR 0134
- Related: issue #738

## Context

StepFun serves `step-5-preview` through its authenticated model-list API, but
models.dev has no record for that model under the first-party StepFun provider.
Its aggregator records disagree on reasoning, tool support and output limits.
Falling across providers can therefore misconfigure a real, runnable model.
The StepFun API publishes a 1,024,000-token input window and low/medium/high
reasoning; its official model guide documents vision, tools and 64k output.

## Decision

Keep models.dev as the general catalog. Introduce one reviewed, source-labelled
supplement for exactly `step-5-preview` at `https://api.stepfun.com/v1`
and the official Step Plan endpoint `https://api.stepfun.com/step_plan/v1`.
The supplement lives in a separate pure module in Electron main. A first-party
models.dev record for the same endpoint/model takes precedence as soon as it
exists; otherwise the supplement precedes cross-provider matches. It is also
available when the bundled catalog cannot load. No network fetch is added to
runtime startup or model lookup.

Metadata carries `provider` provenance, never falsely `models.dev`. The existing
optional model-info catalog-source field and internal runtime source union
accept this additive value. Existing consumers and stored bindings need no
migration. Lookup still goes through the same settings/session/subagent path;
explicit binding overrides remain authoritative.

Only exact HTTPS origin and version path matches qualify. Custom gateways,
lookalike hosts and other model IDs retain their
existing behavior. Live discovery remains the authority on availability: the
supplement does not insert a model into an endpoint's response or claim that
an API key has access. Video capability metadata does not add video attachment
transport to the desktop app.

## Alternatives

- Wait for models.dev: leaves a publicly available model misconfigured.
- Copy a reseller record: repeats inaccurate limits and capabilities.
- Modify the bundled models.dev document: falsely attributes vendor data to
models.dev and loses the change on catalog refresh.
- Introduce a broad parallel model catalog: unnecessary scope and maintenance.

## Consequences

The exception is narrow and removable once first-party catalog coverage is
stable. Its source must be rechecked when StepFun changes the model. The 64k
output ceiling is conservatively represented as 64,000 tokens; no unverified
pricing is supplied. Existing source values, protocols, persistence and secret
ownership remain unchanged.

## References

- [Step 5 Preview guide](https://platform.stepfun.com/docs/zh/guides/models/step-5-preview)
- `GET https://api.stepfun.com/v1/models` (verified 2026-09-21; no credentials
or account data are retained in repository fixtures)
- `apps/desktop/electron/main/stepfun-model-metadata.ts`
4 changes: 4 additions & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ number; use the sidebar when you are exploring a domain.
See the [AI development workflow](/spec/06-delivery/03-ai-development-workflow)
and [change checklist](/spec/06-delivery/05-change-checklist) for the complete
repository rules.

## StepFun

See [StepFun Step 5 Preview setup](stepfun.md).
Loading
Loading