Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 6 additions & 5 deletions apps/desktop/e2e/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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();
Expand All @@ -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();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ProviderAssetMask src=\{stepfunBrandMark\} \/>/);
assert.match(
componentSrc,
/case 'stepfun-step-plan':\s*case 'stepfun-ai':\s*case 'stepfun':\s*return <ProviderAssetMask src=\{stepfunBrandMark\} \/>/,
'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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen
case 'tencent-coding-plan':
case 'tencent-token-plan':
return <ProviderAssetMask src={tencentCloudBrandMark} />;
case 'stepfun-step-plan':
case 'stepfun-ai':
case 'stepfun':
return <ProviderAssetMask src={stepfunBrandMark} />;
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/__tests__/connection-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/__tests__/llm-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('provider compatibility contract', () => {
'nvidia',
'tencent-tokenhub',
'stepfun',
'stepfun-step-plan',
'stepfun-ai',
'volcengine-ark',
'ollama',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Record<string, (typeof PROVIDER_REGISTRY)[keyof typeof PROVIDER_REGISTRY]>>)['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<Record<string, (typeof PROVIDER_REGISTRY)[keyof typeof PROVIDER_REGISTRY]>>)['stepfun-ai'];

Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/__tests__/model-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/__tests__/model-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] });
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/__tests__/provider-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/model-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ const VOLCENGINE_CODING_PLAN_MODEL_METADATA: Record<string, ModelMetadata> = {
const STATIC_MODEL_METADATA: Partial<Record<ProviderType, Record<string, ModelMetadata>>> = {
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']!,
thinkingOptions: { efforts: ['low', 'medium', 'high'] },
},
'step-3.5-flash-2603': {
...GENERATED_MODELS_DEV_METADATA.stepfun['step-3.5-flash-2603']!,
thinkingOptions: { efforts: ['low', 'high'] },
},
'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,
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.',
Expand Down
30 changes: 30 additions & 0 deletions packages/headless/src/__tests__/harbor-cell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 8 additions & 0 deletions packages/headless/src/__tests__/provider-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions packages/headless/src/provider-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand Down
13 changes: 13 additions & 0 deletions packages/runtime/src/__tests__/model-factory-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ describe('buildProviderOptions: thinking level', () => {
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']);
Expand Down
Loading
Loading