From 3d9f7532d7277cc3b4c87ece23c2b022d6b23edb Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 14 Jul 2026 02:52:55 +0800 Subject: [PATCH 1/5] feat(core): register StepFun Step Plan China --- .../src/__tests__/llm-connections.test.ts | 26 ++++++++++++++++ .../core/src/__tests__/model-catalog.test.ts | 30 +++++++++++++++++++ packages/core/src/model-metadata.ts | 12 ++++++++ packages/core/src/provider-registry.ts | 30 +++++++++++++++++++ 4 files changed, 98 insertions(+) diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts index 86c09020426..b4438871fcb 100644 --- a/packages/core/src/__tests__/llm-connections.test.ts +++ b/packages/core/src/__tests__/llm-connections.test.ts @@ -49,6 +49,7 @@ describe('provider compatibility contract', () => { 'nvidia', 'tencent-tokenhub', 'stepfun', + 'stepfun-step-plan', 'stepfun-ai', 'volcengine-ark', 'ollama', @@ -88,6 +89,7 @@ describe('provider compatibility contract', () => { 'volcengine-ark', 'volcengine-coding-plan', 'tencent-token-plan', + 'stepfun-step-plan', ]); assert.deepEqual(CATALOG_PROVIDER_TYPES, [ 'kimi-coding-plan', @@ -118,6 +120,7 @@ describe('provider compatibility contract', () => { 'volcengine-ark', 'volcengine-coding-plan', 'tencent-token-plan', + 'stepfun-step-plan', ]); for (const orderField of ['readyOrder', 'catalogOrder', 'recommendedOrder'] as const) { @@ -435,6 +438,29 @@ describe('provider compatibility contract', () => { ]); }); + it('owns StepFun Step Plan China behavior under the stable stepfun-step-plan id', () => { + const plan = (PROVIDER_REGISTRY as Partial>)['stepfun-step-plan']; + + assert.ok(plan, 'StepFun Step Plan China must have its own persisted provider id'); + assert.equal(plan.label, 'StepFun Step Plan (China)'); + assert.equal(plan.baseUrl, 'https://api.stepfun.com/step_plan/v1'); + assert.equal(plan.authKind, 'api_key'); + assert.equal(plan.protocol, 'openai'); + assert.deepEqual(plan.runtimeAdapter, { kind: 'openai-compatible', name: 'provider' }); + assert.deepEqual(plan.modelDiscovery, { kind: 'fallback' }); + assert.equal(plan.category, 'domestic'); + assert.equal(plan.catalogGroup, 'plans'); + assert.equal(plan.catalogBadge, 'Plan'); + assert.equal(plan.signupUrl, 'https://platform.stepfun.com/interface-key'); + assert.equal(plan.modelsDevId, 'stepfun'); + assert.deepEqual(plan.fallbackModels, [ + 'step-3.7-flash', + 'step-3.5-flash-2603', + 'step-3.5-flash', + 'step-router-v1', + ]); + }); + it('owns StepFun Global direct API behavior under the stable stepfun-ai id', () => { const stepfunGlobal = (PROVIDER_REGISTRY as Partial>)['stepfun-ai']; diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts index eb0995f0802..f5b9e71eba0 100644 --- a/packages/core/src/__tests__/model-catalog.test.ts +++ b/packages/core/src/__tests__/model-catalog.test.ts @@ -9,6 +9,36 @@ import { isConnectionReady } from '../connection-readiness.js'; import type { LlmConnection, ModelInfo, ProviderType } from '../llm-connections.js'; describe('ModelCatalogEntry', () => { + it('uses the official StepFun Step Plan snapshot without inventing live model discovery', () => { + const entries = buildConnectionModelCatalogEntries({ + connection: { + slug: 'stepfun-step-plan', + providerType: 'stepfun-step-plan', + defaultModel: 'step-3.7-flash', + }, + }); + + assert.deepEqual(entries.map((entry) => entry.id), [ + 'step-3.7-flash', + 'step-3.5-flash-2603', + 'step-3.5-flash', + 'step-router-v1', + ]); + assert.equal(entries[0]?.source, 'static_catalog'); + assert.equal(entries[0]?.provenance.modelSource, 'fallback'); + assert.equal(entries[0]?.contextWindow, 256_000); + assert.deepEqual(entries[0]?.capabilities, { + vision: true, + reasoning: true, + functionCalling: true, + }); + assert.equal(entries[3]?.displayName, 'Step Router V1'); + assert.deepEqual(entries[3]?.capabilities, { + reasoning: true, + functionCalling: true, + }); + }); + it('uses the checked-in StepFun Global snapshot until account discovery succeeds', () => { const entries = buildConnectionModelCatalogEntries({ connection: { diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index 26dd9bb9b3b..a2bd2a34eaa 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -125,6 +125,18 @@ const VOLCENGINE_CODING_PLAN_MODEL_METADATA: Record = { const STATIC_MODEL_METADATA: Partial>> = { anthropic: ANTHROPIC_MODEL_OVERRIDES, 'minimax-coding-plan': GENERATED_MODELS_DEV_METADATA.MiniMax, + 'stepfun-step-plan': { + 'step-3.7-flash': GENERATED_MODELS_DEV_METADATA.stepfun['step-3.7-flash']!, + 'step-3.5-flash-2603': GENERATED_MODELS_DEV_METADATA.stepfun['step-3.5-flash-2603']!, + 'step-3.5-flash': GENERATED_MODELS_DEV_METADATA.stepfun['step-3.5-flash']!, + 'step-router-v1': { + displayName: 'Step Router V1', + lifecycle: 'active', + docsUrl: 'https://platform.stepfun.com/docs/zh/step-plan/integrations/reasoning-api', + maxOutputTokens: 384_000, + capabilities: { vision: false, reasoning: true, functionCalling: true }, + }, + }, 'claude-subscription': CLAUDE_SUBSCRIPTION_MODEL_METADATA, openai: OPENAI_MODEL_OVERRIDES, google: GOOGLE_MODEL_OVERRIDES, diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 3aef60c10be..9945e1087e5 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -164,6 +164,17 @@ const stepfunModelIds = toolCallingModelIds( GENERATED_MODELS_DEV_METADATA.stepfun, ['step-3.7-flash', 'step-3.5-flash-2603', 'step-3.5-flash'], ); +const stepfunStepPlanModelIds = [ + 'step-3.7-flash', + 'step-3.5-flash-2603', + 'step-3.5-flash', + 'step-router-v1', +] as const; +for (const id of stepfunStepPlanModelIds.slice(0, 3)) { + if (!GENERATED_MODELS_DEV_METADATA.stepfun[id]?.capabilities?.functionCalling) { + throw new Error(`models.dev StepFun snapshot is missing documented Step Plan model ${id}`); + } +} const stepfunGlobal = GENERATED_MODELS_DEV_PROVIDER_FACTS['stepfun-ai']; if (stepfunGlobal.id !== 'stepfun-ai') { throw new Error('models.dev StepFun Global provider facts are missing stable id stepfun-ai'); @@ -634,6 +645,25 @@ const providerRegistry = { readyOrder: 22, catalogOrder: 22, }, + 'stepfun-step-plan': { + label: 'StepFun Step Plan (China)', + description: 'StepFun subscription access for interactive coding and agent tools in China.', + baseUrl: 'https://api.stepfun.com/step_plan/v1', + authKind: 'api_key', + backendKind: 'ai-sdk', + fallbackModels: [...stepfunStepPlanModelIds], + status: 'ready', + protocol: 'openai', + runtimeAdapter: { kind: 'openai-compatible', name: 'provider' }, + modelDiscovery: { kind: 'fallback' }, + category: 'domestic', + catalogGroup: 'plans', + catalogBadge: 'Plan', + signupUrl: 'https://platform.stepfun.com/interface-key', + modelsDevId: stepfun.id, + readyOrder: 28, + catalogOrder: 28, + }, 'stepfun-ai': { label: stepfunGlobal.name, description: 'StepFun Global models for multimodal reasoning and tool-use agents.', From 91c247d9f7301007d58e26a09c8ba4891ab899ca Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 14 Jul 2026 02:54:01 +0800 Subject: [PATCH 2/5] test(runtime): cover StepFun Step Plan tool loop --- .../__tests__/provider-conformance.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/runtime/src/__tests__/provider-conformance.test.ts b/packages/runtime/src/__tests__/provider-conformance.test.ts index ee476857b3e..9e5926409f1 100644 --- a/packages/runtime/src/__tests__/provider-conformance.test.ts +++ b/packages/runtime/src/__tests__/provider-conformance.test.ts @@ -1550,6 +1550,109 @@ describe('models.dev provider conformance', () => { assert.equal(result.text, 'Echoed hello.'); }); + test('StepFun Step Plan China preserves its snapshot model through the documented two-stage tool-call loop', async () => { + const modelId = 'step-3.7-flash'; + const requestBodies: Array> = []; + const server = await startJsonServer(async (request, response) => { + assert.equal(request.headers.authorization, 'Bearer stepfun-step-plan-test-key'); + assert.equal(request.method, 'POST'); + assert.equal(request.url, '/step_plan/v1/chat/completions'); + const body = JSON.parse(await readBody(request)) as Record; + requestBodies.push(body); + if (requestBodies.length === 1) { + respondJson(response, 200, { + id: 'chatcmpl-stepfun-step-plan-tool', + object: 'chat.completion', + created: 1, + model: modelId, + choices: [{ + index: 0, + message: { + role: 'assistant', + content: '', + reasoning: 'I should call echo with the requested text.', + tool_calls: [{ + id: 'call_stepfun_step_plan_echo', + type: 'function', + function: { name: 'echo', arguments: '{"text":"hello"}' }, + }], + }, + finish_reason: 'tool_calls', + }], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, + }); + return; + } + + respondJson(response, 200, { + id: 'chatcmpl-stepfun-step-plan-final', + object: 'chat.completion', + created: 2, + model: modelId, + choices: [{ + index: 0, + message: { role: 'assistant', content: 'Echoed hello.' }, + finish_reason: 'stop', + }], + usage: { prompt_tokens: 14, completion_tokens: 3, total_tokens: 17 }, + }); + }); + const connection: LlmConnection = { + slug: 'stepfun-step-plan', + name: 'StepFun Step Plan (China)', + providerType: 'stepfun-step-plan', + baseUrl: `${server.url}/step_plan/v1`, + defaultModel: modelId, + enabled: true, + createdAt: 1, + updatedAt: 1, + }; + + const models = await fetchProviderModels(connection, 'stepfun-step-plan-test-key'); + assert.deepEqual(models.map((model) => model.id), [ + 'step-3.7-flash', + 'step-3.5-flash-2603', + 'step-3.5-flash', + 'step-router-v1', + ]); + + const result = await generateText({ + model: getAIModel({ connection, apiKey: 'stepfun-step-plan-test-key', modelId: models[0]!.id }), + prompt: 'Call echo with hello.', + stopWhen: stepCountIs(2), + tools: { + echo: tool({ + description: 'Echo text', + inputSchema: z.object({ text: z.string() }), + execute: async ({ text }) => ({ echoed: text }), + }), + }, + }); + + assert.equal(requestBodies.length, 2, 'snapshot discovery must not call an undocumented /models endpoint'); + assert.equal(result.steps[0]?.reasoningText, 'I should call echo with the requested text.'); + assert.deepEqual(requestBodies.map((body) => body.model), [modelId, modelId]); + assert.deepEqual( + (requestBodies[1]?.messages as Array<{ role: string; content: string }>).find(({ role }) => role === 'tool'), + { role: 'tool', content: '{"echoed":"hello"}', tool_call_id: 'call_stepfun_step_plan_echo' }, + ); + assert.deepEqual( + (requestBodies[1]?.messages as Array>).find(({ role }) => role === 'assistant'), + { + role: 'assistant', + content: null, + reasoning_content: 'I should call echo with the requested text.', + tool_calls: [{ + id: 'call_stepfun_step_plan_echo', + type: 'function', + function: { name: 'echo', arguments: '{"text":"hello"}' }, + }], + }, + ); + assert.deepEqual(result.steps[0]?.toolResults[0]?.output, { echoed: 'hello' }); + assert.equal(result.text, 'Echoed hello.'); + }); + test('Volcengine Ark preserves its snapshot model id through the documented two-stage tool-call loop', async () => { const modelId = 'doubao-seed-2-0-pro-260215'; const requestBodies: Array> = []; From f212c276e704bf06d4e9cc76401466ce230da33b Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 14 Jul 2026 02:56:00 +0800 Subject: [PATCH 3/5] feat: wire StepFun Step Plan credentials across hosts --- .../src/__tests__/connection-target.test.ts | 23 ++++++++++++++ .../core/src/__tests__/provider-auth.test.ts | 13 ++++++++ .../src/__tests__/harbor-cell.test.ts | 30 +++++++++++++++++++ .../src/__tests__/provider-env.test.ts | 8 +++++ packages/headless/src/provider-env.ts | 1 + .../src/__tests__/connection-store.test.ts | 17 +++++++++++ 6 files changed, 92 insertions(+) diff --git a/packages/cli/src/__tests__/connection-target.test.ts b/packages/cli/src/__tests__/connection-target.test.ts index 98331b07c6a..170094ebf88 100644 --- a/packages/cli/src/__tests__/connection-target.test.ts +++ b/packages/cli/src/__tests__/connection-target.test.ts @@ -97,6 +97,29 @@ describe('default session target resolver', () => { assert.equal(target.model, 'step-3.7-flash'); }); + test('resolves StepFun Step Plan credentials without rewriting the selected model id', async () => { + const connection = makeConnection({ + slug: 'stepfun-step-plan', + name: 'StepFun Step Plan (China)', + providerType: 'stepfun-step-plan', + defaultModel: 'step-router-v1', + }); + + const target = await resolveDefaultSessionTarget({ + connectionStore: { + getDefault: async () => 'stepfun-step-plan', + get: async (slug) => slug === 'stepfun-step-plan' ? connection : null, + }, + credentialStore: { + getSecret: async (_slug, kind) => kind === 'api_key' ? 'stepfun-step-plan-test-key' : null, + }, + }); + + assert.equal(target.connection.providerType, 'stepfun-step-plan'); + assert.equal(target.apiKey, 'stepfun-step-plan-test-key'); + assert.equal(target.model, 'step-router-v1'); + }); + test('resolves StepFun Global credentials without rewriting the selected model id', async () => { const connection = makeConnection({ slug: 'stepfun-ai', diff --git a/packages/core/src/__tests__/provider-auth.test.ts b/packages/core/src/__tests__/provider-auth.test.ts index 71575241ea3..9d38f750775 100644 --- a/packages/core/src/__tests__/provider-auth.test.ts +++ b/packages/core/src/__tests__/provider-auth.test.ts @@ -74,6 +74,19 @@ describe('ProviderAuth contract', () => { expect(contract.actionAvailability.fetch_models).toBe('available'); }); + test('StepFun Step Plan China uses an independent API-key credential and snapshot-model flow', () => { + const contract = deriveProviderAuthContract({ + providerType: 'stepfun-step-plan', + hasSecret: true, + }); + + expect(contract.providerType).toBe('stepfun-step-plan'); + expect(contract.setupMode).toBe('api_key'); + expect(contract.requiresSecret).toBe(true); + expect(contract.actionAvailability.test_credentials).toBe('available'); + expect(contract.actionAvailability.fetch_models).toBe('hidden'); + }); + test('Volcengine Ark China uses the shared API-key credential and snapshot-model flow', () => { const contract = deriveProviderAuthContract({ providerType: 'volcengine-ark', diff --git a/packages/headless/src/__tests__/harbor-cell.test.ts b/packages/headless/src/__tests__/harbor-cell.test.ts index 58772a03ded..5097acc235a 100644 --- a/packages/headless/src/__tests__/harbor-cell.test.ts +++ b/packages/headless/src/__tests__/harbor-cell.test.ts @@ -2382,6 +2382,36 @@ setTimeout(() => { assert.equal(missing.apiKey, ''); }); + test('resolves StepFun Step Plan China only from its independent credential env', () => { + const resolved = resolveHarborCellAiSdkEnv({ + provider: 'stepfun-step-plan', + model: 'step-router-v1', + env: { + STEPFUN_STEP_PLAN_API_KEY: 'stepfun-step-plan-key', + STEPFUN_STEP_PLAN_BASE_URL: 'https://api.stepfun.com/step_plan/v1', + STEPFUN_API_KEY: 'direct-key-must-not-cross', + OPENAI_API_KEY: 'openai-key-must-not-cross', + }, + ts: 1, + }); + + assert.equal(resolved.apiKey, 'stepfun-step-plan-key'); + assert.equal(resolved.connection.providerType, 'stepfun-step-plan'); + assert.equal(resolved.connection.defaultModel, 'step-router-v1'); + assert.equal(resolved.connection.baseUrl, 'https://api.stepfun.com/step_plan/v1'); + + const missing = resolveHarborCellAiSdkEnv({ + provider: 'stepfun-step-plan', + model: 'step-router-v1', + env: { + STEPFUN_API_KEY: 'direct-key-must-not-cross', + OPENAI_API_KEY: 'openai-key-must-not-cross', + }, + ts: 1, + }); + assert.equal(missing.apiKey, ''); + }); + test('resolves StepFun Global only from its independent direct API credential env', () => { const resolved = resolveHarborCellAiSdkEnv({ provider: 'stepfun-ai', diff --git a/packages/headless/src/__tests__/provider-env.test.ts b/packages/headless/src/__tests__/provider-env.test.ts index 1128e80f712..e0ccb3a56da 100644 --- a/packages/headless/src/__tests__/provider-env.test.ts +++ b/packages/headless/src/__tests__/provider-env.test.ts @@ -103,6 +103,14 @@ test('StepFun China keeps direct API credentials separate from global and plan i }); }); +test('StepFun Step Plan China uses an independent credential namespace', () => { + assert.deepEqual(providerCredentialEnv('stepfun-step-plan'), { + apiKeys: ['STEPFUN_STEP_PLAN_API_KEY'], + apiKeyFile: 'STEPFUN_STEP_PLAN_API_KEY_FILE', + baseUrls: ['STEPFUN_STEP_PLAN_BASE_URL'], + }); +}); + test('StepFun Global keeps direct API credentials separate from China and plan identities', () => { assert.deepEqual(providerCredentialEnv('stepfun-ai'), { apiKeys: ['STEPFUN_AI_API_KEY'], diff --git a/packages/headless/src/provider-env.ts b/packages/headless/src/provider-env.ts index 7e875ab3bf2..6f1e83ea39e 100644 --- a/packages/headless/src/provider-env.ts +++ b/packages/headless/src/provider-env.ts @@ -26,6 +26,7 @@ const PROVIDER_CREDENTIAL_ENV = { nvidia: env('NVIDIA', ['NVIDIA_BASE_URL']), 'tencent-tokenhub': env('TENCENT_TOKENHUB', ['TENCENT_TOKENHUB_BASE_URL']), stepfun: env('STEPFUN', ['STEPFUN_BASE_URL']), + 'stepfun-step-plan': env('STEPFUN_STEP_PLAN', ['STEPFUN_STEP_PLAN_BASE_URL']), 'stepfun-ai': env('STEPFUN_AI', ['STEPFUN_AI_BASE_URL']), 'volcengine-ark': env('ARK', ['ARK_BASE_URL']), localai: env('LOCALAI', ['LOCALAI_BASE_URL']), diff --git a/packages/storage/src/__tests__/connection-store.test.ts b/packages/storage/src/__tests__/connection-store.test.ts index f0602a380d7..8f582b4f066 100644 --- a/packages/storage/src/__tests__/connection-store.test.ts +++ b/packages/storage/src/__tests__/connection-store.test.ts @@ -136,6 +136,23 @@ describe('FileConnectionStore', () => { }); }); + test('persists the StepFun Step Plan China provider id and exact default model', async () => { + await withConnectionStore(async (store, dir) => { + await store.create({ + slug: 'stepfun-step-plan', + name: 'StepFun Step Plan (China)', + providerType: 'stepfun-step-plan', + defaultModel: 'step-router-v1', + }); + + const persisted = JSON.parse(await readFile(join(dir, 'llm-connections.json'), 'utf8')) as { + connections: Array<{ providerType: string; defaultModel: string }>; + }; + assert.equal(persisted.connections[0]?.providerType, 'stepfun-step-plan'); + assert.equal(persisted.connections[0]?.defaultModel, 'step-router-v1'); + }); + }); + test('persists the StepFun Global provider id and exact default model', async () => { await withConnectionStore(async (store, dir) => { await store.create({ From 29c5b0f8923ab9e91e098a870e621b219d1f08f3 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 14 Jul 2026 02:57:08 +0800 Subject: [PATCH 4/5] feat(desktop): brand StepFun Step Plan --- apps/desktop/e2e/providers.spec.ts | 11 ++++++----- .../main/__tests__/icon-governance-contract.test.ts | 5 +++++ .../src/renderer/settings/provider-brand-marks.tsx | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/desktop/e2e/providers.spec.ts b/apps/desktop/e2e/providers.spec.ts index 6f182e9a565..05c4bc86a9b 100644 --- a/apps/desktop/e2e/providers.spec.ts +++ b/apps/desktop/e2e/providers.spec.ts @@ -357,15 +357,16 @@ test('adds Tencent TokenHub with its exact snapshot model, API-key field, and sh }); for (const stepfun of [ - { label: 'StepFun (China)', providerType: 'stepfun', baseUrl: 'https://api.stepfun.com/v1' }, - { label: 'StepFun (Global)', providerType: 'stepfun-ai', baseUrl: 'https://api.stepfun.ai/v1' }, + { label: 'StepFun (China)', providerType: 'stepfun', baseUrl: 'https://api.stepfun.com/v1', tab: 'API', model: 'step-3.7-flash' }, + { label: 'StepFun Step Plan (China)', providerType: 'stepfun-step-plan', baseUrl: 'https://api.stepfun.com/step_plan/v1', tab: '模型计划', model: 'step-3.7-flash' }, + { label: 'StepFun (Global)', providerType: 'stepfun-ai', baseUrl: 'https://api.stepfun.ai/v1', tab: 'API', model: 'step-3.7-flash' }, ] as const) test(`adds ${stepfun.label} with its exact snapshot model, API-key field, and shared official mark`, async ({ window: page }) => { await page.getByRole('button', { name: '展开侧边栏' }).click(); await page.getByRole('button', { name: '设置' }).click(); await page.locator('[aria-label="设置分组"]').getByText('模型', { exact: true }).click(); await page.getByRole('button', { name: '添加服务商' }).click(); - await page.getByRole('tab', { name: 'API', exact: true }).click(); + await page.getByRole('tab', { name: stepfun.tab, exact: true }).click(); await page.getByPlaceholder('搜索服务商').fill('StepFun'); const catalogMark = page.locator( `.providerCatalogRow[data-provider="${stepfun.providerType}"] .providerLogo .providerAssetMask`, @@ -376,7 +377,7 @@ for (const stepfun of [ await expect(page.getByLabel('模型供应商连接标识')).toHaveValue(stepfun.providerType); await expect(page.getByLabel('模型供应商服务地址')).toHaveValue(stepfun.baseUrl); - await expect(page.getByLabel('模型供应商默认模型')).toHaveValue('step-3.7-flash'); + await expect(page.getByLabel('模型供应商默认模型')).toHaveValue(stepfun.model); await page.getByRole('button', { name: '保存供应商' }).click(); await expect(page.getByRole('heading', { name: stepfun.label, exact: true }).first()).toBeVisible(); @@ -385,7 +386,7 @@ for (const stepfun of [ ); await expect(detailMark).toBeVisible(); expect(await detailMark.evaluate(maskRenderContract)).toEqual({ usesAssetMask: true, followsForeground: true }); - await expect(page.getByText('step-3.7-flash', { exact: true }).first()).toBeVisible(); + await expect(page.getByText(stepfun.model, { exact: true }).first()).toBeVisible(); await expect(page.getByRole('textbox', { name: '模型密钥' })).toBeVisible(); }); diff --git a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts index ae23dadc553..fbd407e8c1b 100644 --- a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts +++ b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts @@ -433,6 +433,11 @@ describe('icon + typography governance contract', () => { /StepFun mark vendored byte-for-byte from Lobe Icons:[\s\S]*@lobehub\/icons-static-svg@1\.91\.0[\s\S]*e4302041fbb3039608d25f9f618bd462783b875e[\s\S]*packages\/static-svg\/icons\/stepfun\.svg[\s\S]*MIT[\s\S]*f46fbd1eee00a3dc7874395484bcc3e25a803e9eb4b79f07b7eec377a1e2f25c/, ); assert.match(componentSrc, /case 'stepfun':\s*return /); + assert.match( + componentSrc, + /case 'stepfun-step-plan':\s*case 'stepfun-ai':\s*case 'stepfun':\s*return /, + 'catalog and detail must route every StepFun access path through the same shared mark and mask seam', + ); }); it('vendors and routes the byte-exact upstream Volcengine mark through the shared asset-mask seam', async () => { diff --git a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx index 8991bafa5b6..4e455a4572c 100644 --- a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx +++ b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx @@ -295,6 +295,7 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen case 'tencent-coding-plan': case 'tencent-token-plan': return ; + case 'stepfun-step-plan': case 'stepfun-ai': case 'stepfun': return ; From 5d63ebd7b008417c36317cad48adc048877bdc61 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 14 Jul 2026 03:01:55 +0800 Subject: [PATCH 5/5] feat: wire StepFun Step Plan reasoning effort --- packages/core/src/__tests__/model-thinking.test.ts | 10 ++++++++++ packages/core/src/model-metadata.ts | 10 ++++++++-- .../src/__tests__/model-factory-thinking.test.ts | 13 +++++++++++++ packages/runtime/src/model-factory.ts | 1 + 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/model-thinking.test.ts b/packages/core/src/__tests__/model-thinking.test.ts index c577b0deeff..2abb5b31ff3 100644 --- a/packages/core/src/__tests__/model-thinking.test.ts +++ b/packages/core/src/__tests__/model-thinking.test.ts @@ -58,6 +58,16 @@ describe('deriveThinkingChoices', () => { }); describe('thinkingOptionsForModel', () => { + test('StepFun Step Plan declares official effort levels per exact model id', () => { + assert.deepEqual(thinkingOptionsForModel('stepfun-step-plan', 'step-3.7-flash'), { + efforts: ['low', 'medium', 'high'], + }); + assert.deepEqual(thinkingOptionsForModel('stepfun-step-plan', 'step-3.5-flash-2603'), { + efforts: ['low', 'high'], + }); + assert.equal(thinkingOptionsForModel('stepfun-step-plan', 'step-router-v1'), undefined); + }); + test('openai gpt-5.5 exposes none/low/medium/high/xhigh; gpt-5 exposes minimal/low/medium/high', () => { assert.deepEqual(thinkingOptionsForModel('openai', 'gpt-5.5'), { efforts: ['none', 'low', 'medium', 'high', 'xhigh'] }); assert.deepEqual(thinkingOptionsForModel('openai', 'gpt-5'), { efforts: ['minimal', 'low', 'medium', 'high'] }); diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index a2bd2a34eaa..09c6b159da6 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -126,8 +126,14 @@ const STATIC_MODEL_METADATA: Partial { assert.deepEqual(buildProviderOptions(conn('deepseek'), 'deepseek-chat', 'high'), {}); }); + test('StepFun Step Plan sends only officially supported reasoning effort levels', () => { + assert.deepEqual( + buildProviderOptions(conn('stepfun-step-plan'), 'step-3.7-flash', 'medium'), + { 'stepfun-step-plan': { reasoningEffort: 'medium' } }, + ); + assert.deepEqual( + buildProviderOptions(conn('stepfun-step-plan'), 'step-3.5-flash-2603', 'high'), + { 'stepfun-step-plan': { reasoningEffort: 'high' } }, + ); + assert.deepEqual(buildProviderOptions(conn('stepfun-step-plan'), 'step-3.5-flash-2603', 'medium'), {}); + assert.deepEqual(buildProviderOptions(conn('stepfun-step-plan'), 'step-router-v1', 'high'), {}); + }); + test('Volcengine Ark sends its official thinking object and optional reasoning effort', () => { const modelId = 'doubao-seed-2-0-pro-260215'; assert.deepEqual([...thinkingVariantsForModel('volcengine-ark', modelId)], ['off', 'minimal', 'low', 'medium', 'high']); diff --git a/packages/runtime/src/model-factory.ts b/packages/runtime/src/model-factory.ts index ea2fcb726ba..9aff26ed8e4 100644 --- a/packages/runtime/src/model-factory.ts +++ b/packages/runtime/src/model-factory.ts @@ -150,6 +150,7 @@ export function buildProviderOptions( case 'moonshot': case 'tencent-token-plan': case 'zai-coding-plan': + case 'stepfun-step-plan': return level && level !== 'off' ? { [openaiCompatibleNamespace(connection.providerType)]: { reasoningEffort: level } } : {};