From 5049c68a8c58a1af037a2f95540f33819a3a2c5d Mon Sep 17 00:00:00 2001 From: longyongshen Date: Thu, 15 Oct 2026 11:20:49 +0800 Subject: [PATCH] fix: always use system messages for StepFun chat models --- .../coding-agent/test/step-provider.test.ts | 42 ++++++++++++++++--- packages/providers/src/step-provider/index.ts | 19 ++++++--- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/test/step-provider.test.ts b/packages/coding-agent/test/step-provider.test.ts index ba77a356..7aec3a6f 100644 --- a/packages/coding-agent/test/step-provider.test.ts +++ b/packages/coding-agent/test/step-provider.test.ts @@ -104,16 +104,26 @@ describe("Step provider extension", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 256_000, + compat: { supportsDeveloperRole: true, supportsReasoningEffort: false }, }, ]; const config = createStepProviderConfig({ models, env: {} }); expect(config.models?.[0]?.api).toBe("openai-completions"); expect(config.models?.[0]?.input).toEqual(["text", "image"]); + expect(config.models?.[0]?.compat).toEqual({ supportsDeveloperRole: false, supportsReasoningEffort: false }); expect(models[0]).not.toHaveProperty("api"); + expect(models[0]?.compat.supportsDeveloperRole).toBe(true); }); - it("routes a stored Step login to the chat completions endpoint exactly once", async () => { + it.each([ + "https://api.stepfun.com/step_plan", + "https://api.stepfun.com", + "https://api.stepfun.ai/step_plan", + "https://api.stepfun.ai", + "https://step-proxy.example", + ])("sends system instructions to the Step chat endpoint at %s", async (apiBaseUrl) => { + const baseUrl = stepOpenAiBaseUrl(apiBaseUrl); const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory({ [STEP_PROVIDER_ID]: { @@ -129,6 +139,7 @@ describe("Step provider extension", () => { runtime.registerProvider( STEP_PROVIDER_ID, createStepProviderConfig({ + apiBaseUrl, env: {}, models: [ { @@ -146,25 +157,32 @@ describe("Step provider extension", () => { const model = runtime.getModel(STEP_PROVIDER_ID, "step-3.7-flash"); expect(model).toMatchObject({ api: "openai-completions", - baseUrl: "https://api.stepfun.com/step_plan/v1", + baseUrl, + compat: { supportsDeveloperRole: false }, }); let requestUrl: string | undefined; let requestHeaders: Headers | undefined; + let requestMessages: unknown; const response = await runtime.completeSimple( model!, - { messages: [{ role: "user", content: "hello", timestamp: 1 }] }, + { systemPrompt: "You are StepCode.", messages: [{ role: "user", content: "hello", timestamp: 1 }] }, { fetch: async (input, init) => { requestUrl = String(input); requestHeaders = new Headers(init?.headers); + requestMessages = JSON.parse(String(init?.body)).messages; return new Response(null, { status: 401 }); }, }, ); - expect(requestUrl).toBe("https://api.stepfun.com/step_plan/v1/chat/completions"); + expect(requestUrl).toBe(`${baseUrl}/chat/completions`); expect(requestHeaders?.get("authorization")).toBe("Bearer step-key"); + expect(requestMessages).toEqual([ + { role: "system", content: "You are StepCode." }, + { role: "user", content: "hello" }, + ]); expect(response.errorMessage).toContain("401"); }); @@ -183,6 +201,8 @@ describe("Step provider extension", () => { { id: "step-3.7-flash", api: "anthropic-messages", + reasoning: true, + compat: { supportsDeveloperRole: true, supportsReasoningEffort: false }, // The dialect is already correct, but an old proxy host must // not survive as a model-level override. baseUrl: "https://models-proxy.example/v1/messages", @@ -208,19 +228,26 @@ describe("Step provider extension", () => { runtime.registerProvider(STEP_PROVIDER_ID, createStepProviderConfig({ env: {} })); const model = runtime.getModel(STEP_PROVIDER_ID, "step-3.7-flash"); expect(model?.baseUrl).toBe("https://api.stepfun.com/step_plan/v1"); + expect(model?.compat).toMatchObject({ supportsDeveloperRole: false, supportsReasoningEffort: false }); let requestUrl: string | undefined; + let requestMessages: unknown; await runtime.completeSimple( model!, - { messages: [{ role: "user", content: "hello", timestamp: 1 }] }, + { systemPrompt: "You are StepCode.", messages: [{ role: "user", content: "hello", timestamp: 1 }] }, { - fetch: async (input) => { + fetch: async (input, init) => { requestUrl = String(input); + requestMessages = JSON.parse(String(init?.body)).messages; return new Response(null, { status: 404 }); }, }, ); expect(requestUrl).toBe("https://api.stepfun.com/step_plan/v1/chat/completions"); + expect(requestMessages).toEqual([ + { role: "system", content: "You are StepCode." }, + { role: "user", content: "hello" }, + ]); } finally { await rm(root, { recursive: true, force: true }); } @@ -498,6 +525,9 @@ describe("Step dynamic model discovery", () => { input: ["text"], }), ]); + for (const model of models) { + expect(model.compat).toMatchObject({ supportsDeveloperRole: false }); + } }); it("returns no models when unauthenticated or offline", async () => { diff --git a/packages/providers/src/step-provider/index.ts b/packages/providers/src/step-provider/index.ts index d8fbeba4..eeb3b0e4 100644 --- a/packages/providers/src/step-provider/index.ts +++ b/packages/providers/src/step-provider/index.ts @@ -119,7 +119,7 @@ interface StepModelMeta { function stepModelConfig(id: string, name: string, baseUrl?: string, meta?: StepModelMeta): StepModelConfig { const contextWindow = meta?.contextWindow ?? STEP_MODEL_DEFAULTS.contextWindow; const hasEfforts = (meta?.efforts?.length ?? 0) > 0; - return { + return normalizeStepModelConfig({ id, name, api: STEP_MODEL_API, @@ -135,7 +135,7 @@ function stepModelConfig(id: string, name: string, baseUrl?: string, meta?: Step // When the list endpoint reports supported efforts (e.g. step_plan), carry // them so `/effort` is correct without a per-model lookup. ...(hasEfforts ? { thinkingLevelMap: stepThinkingLevelMap(meta!.efforts!) } : {}), - }; + }); } /** @@ -246,22 +246,29 @@ export function resolveStepProviderOptions(options: StepProviderOptions = {}): R /** * A model entry may omit its dialect. Default it to the shared Step wire * dialect (OpenAI Chat Completions) so a bare custom entry streams correctly. + * Step chat endpoints require the system role even for reasoning models. */ export function normalizeStepModelConfig(model: StepModelConfig): StepModelConfig { return { ...model, api: model.api ?? STEP_MODEL_API, input: [...model.input], + compat: { ...model.compat, supportsDeveloperRole: false }, }; } /** * Every Step model uses the active profile's OpenAI endpoint and dialect. - * Restore both so a stale models.json overlay (e.g. an old proxy host or the - * legacy Anthropic dialect) cannot misroute a Step model. + * Restore these and the system-role requirement so a stale models.json overlay + * cannot misroute a Step model or send an unsupported developer role. */ export function normalizeStepModel(model: Model, openaiBaseUrl: string): Model { - return { ...model, api: STEP_MODEL_API, baseUrl: openaiBaseUrl }; + return { + ...model, + api: STEP_MODEL_API, + baseUrl: openaiBaseUrl, + compat: { ...model.compat, supportsDeveloperRole: false }, + }; } /** Cloud login has one path: init, open the supplied URL, poll, store the existing API key. */ @@ -442,7 +449,7 @@ export async function fetchStepModels( context: RefreshModelsContext, resolved: ResolvedStepProviderOptions, ): Promise { - const baseline = (): StepModelConfig[] => resolved.models.map((model) => ({ ...model, input: [...model.input] })); + const baseline = (): StepModelConfig[] => resolved.models.map(normalizeStepModelConfig); if (!context.allowNetwork || context.signal.aborted) return baseline(); const token = readStepBearerToken(context.credential); if (!token) return baseline();