diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 138b4e27b..5394de918 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -18,6 +18,7 @@ ### Fixed +- Fixed `models.json` typings omitting the documented OpenAI-compatible `compat.supportsFinishReason` provider and model override ([#8460](https://github.com/earendil-works/pi/issues/8460)). - Fixed writes to `auth.json` and `models-store.json` overriding administrator-managed file permissions and ACLs ([#7779](https://github.com/earendil-works/pi/issues/7779)). - Fixed UTF-8 BOM markers preventing frontmatter and user configuration files from loading ([#8337](https://github.com/earendil-works/pi/issues/8337)). - Fixed invalid settings files being easy to miss during interactive startup by rendering warnings with the file path inside the TUI ([#7829](https://github.com/earendil-works/pi/issues/7829)). diff --git a/packages/coding-agent/src/core/model-config.ts b/packages/coding-agent/src/core/model-config.ts index f24a27937..6036d5af9 100644 --- a/packages/coding-agent/src/core/model-config.ts +++ b/packages/coding-agent/src/core/model-config.ts @@ -75,6 +75,7 @@ const OpenAICompletionsCompatSchema = Type.Object({ supportsDeveloperRole: Type.Optional(Type.Boolean()), supportsReasoningEffort: Type.Optional(Type.Boolean()), supportsUsageInStreaming: Type.Optional(Type.Boolean()), + supportsFinishReason: Type.Optional(Type.Boolean()), maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])), requiresToolResultName: Type.Optional(Type.Boolean()), requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()), diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index dae695c33..18bf921b3 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -11,6 +11,7 @@ import type { import { getApiProvider, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; +import type { ModelsJsonProvider } from "../src/core/model-config.ts"; import { clearApiKeyCache, type ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts"; import { createModelRegistry } from "./model-runtime-test-utils.ts"; @@ -767,6 +768,26 @@ describe("ModelRegistry", () => { expect(compat?.openRouterRouting).toEqual({ only: ["amazon-bedrock"] }); }); + test("supportsFinishReason can be configured at provider and model levels", async () => { + const provider: ModelsJsonProvider = { + compat: { supportsFinishReason: true }, + modelOverrides: { + "anthropic/claude-sonnet-4": { + compat: { supportsFinishReason: false }, + }, + }, + }; + writeRawModelsJson({ openrouter: provider }); + + const registry = await createModelRegistry(authStorage, modelsJsonPath); + const models = getModelsForProvider(registry, "openrouter"); + const sonnet = models.find((model) => model.id === "anthropic/claude-sonnet-4"); + const opus = models.find((model) => model.id === "anthropic/claude-opus-4"); + + expect((sonnet?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(false); + expect((opus?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(true); + }); + test("model override deep merges compat settings", async () => { writeRawModelsJson({ openrouter: {