diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..30ab299d --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +link-workspace-packages=true diff --git a/CHANGELOG.md b/CHANGELOG.md index fc69dd7c..02ba80a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.6.3 + +### Hotfix + +- 修复 `@actalk/inkos@1.6.2` / `@actalk/inkos-studio@1.6.2` 发布到 npm 时 registry manifest 泄漏 `workspace:*` 的问题;Windows / npm 全局升级请直接安装 `1.6.3` 或更新到 `latest` +- 发布校验现在会拒绝 publishable manifest 中的 `workspace:` 依赖,避免同类安装错误复发 +- MiniMax 官方 OpenAI-compatible 接入新增 `MiniMax-M3` 模型卡,并对 `MiniMax-M3*` 默认发送 `thinking: { "type": "disabled" }`,减少接口默认返回 thinking 内容的问题 + ## v1.6.2 ### Release Focus diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f2179e6..34ee9634 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ Keep commits atomic — one logical change per commit. Split new files, interfac - Immutable patterns: `{ ...obj, key: value }` over mutation - Functions < 50 lines, files < 800 lines - Errors must surface, not be swallowed (`catch { }` without re-throw needs a comment) -- `workspace:*` stays in source `package.json` — the CI pipeline handles version replacement at publish time +- Publishable package manifests must use registry-installable internal versions, not `workspace:*`; `pnpm` links local packages through the workspace config during development. ## Adding a CLI Command diff --git a/README.en.md b/README.en.md index 54f3c18b..7e848040 100644 --- a/README.en.md +++ b/README.en.md @@ -165,6 +165,8 @@ Open Studio, then go to **Model Settings**: Studio uses project service settings and `.inkos/secrets.json`. It may show env-detection hints, but env files do not override the Studio-selected service/model/base URL/API key. +MiniMax uses the official OpenAI-compatible `/v1/chat/completions` endpoint. InkOS disables returned thinking by default for `MiniMax-M3*`; M2.x thinking cannot be disabled by the upstream service. + **Option 2: CLI / daemon / deployment env config** ```bash diff --git a/README.ja.md b/README.ja.md index f3f1f129..aa48f7c4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -130,6 +130,8 @@ Studio を開き、**モデル設定**へ進みます: Studio はプロジェクトのサービス設定と `.inkos/secrets.json` を使います。env が検出されてもヒントとして表示するだけで、Studio で選んだ service / model / base URL / API Key を上書きしません。 +MiniMax は公式 OpenAI-compatible `/v1/chat/completions` エンドポイントを使用します。InkOS は `MiniMax-M3*` の thinking 返却をデフォルトで無効化します。M2.x の thinking は上流サービス側の制限により無効化できません。 + **方法2:CLI / daemon / デプロイ環境の env 設定** ```bash diff --git a/README.md b/README.md index 2af3b59d..244a875a 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ inkos doctor | `legacy-env` | 旧 env 模式:兼容老项目的纯 `.env` 配置 | -如果服务测试失败,优先检查服务商、模型和协议是否匹配。Google Gemini 的 AI Studio API Key 可用于 Gemini OpenAI-compatible endpoint;InkOS 会自动禁用 Google 不支持的 OpenAI `store` 参数。MiniMax / MiniMax CodingPlan 默认走官方 OpenAI-compatible `/v1/chat/completions`,并优先使用可工作的非流式 transport,避免流式返回 usage 但无正文的问题。 +如果服务测试失败,优先检查服务商、模型和协议是否匹配。Google Gemini 的 AI Studio API Key 可用于 Gemini OpenAI-compatible endpoint;InkOS 会自动禁用 Google 不支持的 OpenAI `store` 参数。MiniMax 默认走官方 OpenAI-compatible `/v1/chat/completions`,并优先使用可工作的非流式 transport,避免流式返回 usage 但无正文的问题;`MiniMax-M3*` 会默认关闭 thinking 返回,M2.x thinking 由上游限制无法关闭。 ### LLM 配置更新 diff --git a/package.json b/package.json index 482ecdb5..3a5140ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inkos", - "version": "1.6.2", + "version": "1.6.3", "private": true, "description": "Story Creation AI Agent for long-form and short fiction, scripts, interactive games, and IP content", "keywords": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index a83bbbcb..536fa08f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@actalk/inkos", - "version": "1.6.2", + "version": "1.6.3", "description": "Story Creation AI Agent CLI for long-form fiction, InkOS Short, fan works, continuations, covers, and interactive worlds.", "keywords": [ "story-creation-ai-agent", @@ -49,8 +49,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@actalk/inkos-core": "workspace:*", - "@actalk/inkos-studio": "workspace:*", + "@actalk/inkos-core": "1.6.3", + "@actalk/inkos-studio": "1.6.3", "commander": "^13.0.0", "dotenv": "^16.4.0", "epub-gen-memory": "^1.0.10", diff --git a/packages/cli/src/__tests__/publish-package.test.ts b/packages/cli/src/__tests__/publish-package.test.ts index 15ab429b..01eb625d 100644 --- a/packages/cli/src/__tests__/publish-package.test.ts +++ b/packages/cli/src/__tests__/publish-package.test.ts @@ -117,11 +117,13 @@ describe.sequential("publish packaging", () => { } }); - it("keeps source CLI dependencies linked through the workspace protocol", async () => { + it("keeps source publish dependencies registry-installable", async () => { const cliPackageJson = await sourceCliPackageJsonPromise; + const studioPackageJson = await sourceStudioPackageJsonPromise; - expect(cliPackageJson.dependencies["@actalk/inkos-core"]).toBe("workspace:*"); - expect(cliPackageJson.dependencies["@actalk/inkos-studio"]).toBe("workspace:*"); + expect(cliPackageJson.dependencies["@actalk/inkos-core"]).not.toMatch(/^workspace:/); + expect(cliPackageJson.dependencies["@actalk/inkos-studio"]).not.toMatch(/^workspace:/); + expect(studioPackageJson.dependencies["@actalk/inkos-core"]).not.toMatch(/^workspace:/); }); it("verifies publishable manifests before npm publish runs", async () => { @@ -138,7 +140,7 @@ describe.sequential("publish packaging", () => { ); }); - it("allows source workspace protocol manifests when they normalize cleanly for publish", async () => { + it("rejects workspace protocol in source publish manifests", async () => { const tempRoot = await mkdtemp(join(tmpdir(), "inkos-publish-verify-pass-")); const tempPackagesDir = join(tempRoot, "packages"); const tempCoreDir = join(tempPackagesDir, "core"); @@ -182,7 +184,7 @@ describe.sequential("publish packaging", () => { encoding: "utf-8", stdio: "pipe", }, - )).not.toThrow(); + )).toThrow(/workspace protocol is not allowed/); } finally { await rm(tempRoot, { recursive: true, force: true }); } @@ -231,7 +233,7 @@ describe.sequential("publish packaging", () => { encoding: "utf-8", stdio: "pipe", }, - )).toThrow(/normalizes to 0\.5\.0, expected 0\.5\.1/); + )).toThrow(/workspace protocol is not allowed/); } finally { await rm(tempRoot, { recursive: true, force: true }); } diff --git a/packages/core/package.json b/packages/core/package.json index a94aeecf..6c17e8dd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@actalk/inkos-core", - "version": "1.6.2", + "version": "1.6.3", "description": "InkOS core engine for story creation agents: governed context, writing, audit, revision, covers, and interactive worlds", "keywords": [ "story-creation-ai-agent", diff --git a/packages/core/src/__tests__/provider-minimax-thinking.test.ts b/packages/core/src/__tests__/provider-minimax-thinking.test.ts new file mode 100644 index 00000000..baf5de6c --- /dev/null +++ b/packages/core/src/__tests__/provider-minimax-thinking.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from "vitest"; +import { chatCompletion, createLLMClient } from "../llm/provider.js"; + +const { fetchCalls } = vi.hoisted(() => ({ + fetchCalls: [] as Array<{ url: string; init: RequestInit; body: Record }>, +})); + +vi.mock("../utils/proxy-fetch.js", () => ({ + fetchWithProxy: vi.fn(async (url: string, init: RequestInit) => { + fetchCalls.push({ + url, + init, + body: JSON.parse(String(init.body ?? "{}")), + }); + return { + ok: true, + json: async () => ({ + choices: [{ message: { content: "ok" } }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + } as Response; + }), +})); + +vi.mock("@mariozechner/pi-ai", () => ({ + completeSimple: vi.fn(async () => { + throw new Error("MiniMax OpenAI-compatible requests must use InkOS native transport"); + }), + streamSimple: vi.fn(async function* () { + throw new Error("MiniMax OpenAI-compatible requests must use InkOS native transport"); + }), +})); + +function minimaxClient(model: string) { + return createLLMClient({ + provider: "openai", + service: "minimax", + model, + apiKey: "sk-test", + apiFormat: "chat", + stream: false, + temperature: 0.9, + thinkingBudget: 0, + extra: {}, + } as never); +} + +describe("MiniMax thinking defaults", () => { + it("disables MiniMax-M3 thinking by default on the OpenAI-compatible endpoint", async () => { + fetchCalls.length = 0; + const client = minimaxClient("MiniMax-M3"); + + const result = await chatCompletion(client, "MiniMax-M3", [ + { role: "user", content: "hi" }, + ], { retry: false }); + + expect(result.content).toBe("ok"); + expect(fetchCalls).toHaveLength(1); + expect(fetchCalls[0]!.url).toBe("https://api.minimaxi.com/v1/chat/completions"); + expect(fetchCalls[0]!.body).toMatchObject({ + model: "MiniMax-M3", + thinking: { type: "disabled" }, + }); + }); + + it("does not send unsupported thinking controls to MiniMax-M2.x models", async () => { + fetchCalls.length = 0; + const client = minimaxClient("MiniMax-M2.7"); + + await chatCompletion(client, "MiniMax-M2.7", [ + { role: "user", content: "hi" }, + ], { retry: false }); + + expect(fetchCalls).toHaveLength(1); + expect(fetchCalls[0]!.body).not.toHaveProperty("thinking"); + }); +}); diff --git a/packages/core/src/__tests__/service-presets-regression.test.ts b/packages/core/src/__tests__/service-presets-regression.test.ts index 87f4b500..0abef2a3 100644 --- a/packages/core/src/__tests__/service-presets-regression.test.ts +++ b/packages/core/src/__tests__/service-presets-regression.test.ts @@ -31,10 +31,11 @@ describe("service-presets regression", () => { expect(preset!.api).toBe("openai-completions"); }); - it("has knownModels with all 7 MiniMax models", () => { + it("has knownModels with current MiniMax chat models", () => { const preset = resolveServicePreset("minimax"); expect(preset!.knownModels).toBeDefined(); - expect(preset!.knownModels).toHaveLength(7); + expect(preset!.knownModels).toHaveLength(8); + expect(preset!.knownModels).toContain("MiniMax-M3"); expect(preset!.knownModels).toContain("MiniMax-M2.7"); expect(preset!.knownModels).toContain("MiniMax-M2.7-highspeed"); expect(preset!.knownModels).toContain("MiniMax-M2.5"); @@ -62,6 +63,7 @@ describe("service-presets regression", () => { it("returns provider bank models for minimax (B8 升级:provider.models 替代 preset.knownModels)", async () => { const models = await listModelsForService("minimax"); expect(models.length).toBeGreaterThanOrEqual(7); + expect(models.some((m) => m.id === "MiniMax-M3")).toBe(true); expect(models.some((m) => m.id === "MiniMax-M2.7")).toBe(true); }); diff --git a/packages/core/src/llm/provider.ts b/packages/core/src/llm/provider.ts index dce1f071..8f177fbc 100644 --- a/packages/core/src/llm/provider.ts +++ b/packages/core/src/llm/provider.ts @@ -616,6 +616,9 @@ async function withTransientLLMRetry( } function shouldUseNativeCustomTransport(client: LLMClient): boolean { + if (client.service === "minimax" && client.provider === "openai") { + return true; + } if (client.service === "kkaiapi" && client.provider === "openai") { return true; } @@ -650,6 +653,16 @@ function buildCustomHeaders(client: LLMClient): Record { }) ?? { "Content-Type": "application/json" }; } +function defaultOpenAIChatExtra(client: LLMClient, model: string): Record { + if ( + client.service === "minimax" + && /^minimax-m3(?:$|[-_.])/i.test(model) + ) { + return { thinking: { type: "disabled" } }; + } + return {}; +} + function sanitizeHeaderApiKey(apiKey: string | undefined): string { const trimmed = apiKey?.trim() ?? ""; if (!trimmed) return ""; @@ -1041,6 +1054,7 @@ async function chatCompletionViaCustomOpenAICompatible( stream: client.stream, temperature: resolved.temperature, max_tokens: resolved.maxTokens, + ...defaultOpenAIChatExtra(client, model), ...extra, }; if (client.stream) { diff --git a/packages/core/src/llm/providers/endpoints/minimax.ts b/packages/core/src/llm/providers/endpoints/minimax.ts index d673df52..69a1d207 100644 --- a/packages/core/src/llm/providers/endpoints/minimax.ts +++ b/packages/core/src/llm/providers/endpoints/minimax.ts @@ -24,6 +24,7 @@ export const MINIMAX: InkosEndpoint = { defaultTemperature: 0.9, writingTemperature: 0.9, models: [ + { id: "MiniMax-M3", maxOutput: 131072, contextWindowTokens: 1_000_000, enabled: true, releasedAt: "2026-07-01" }, { id: "MiniMax-M2.7", maxOutput: 131072, contextWindowTokens: 204800, enabled: true, releasedAt: "2026-03-18" }, { id: "MiniMax-M2.7-highspeed", maxOutput: 131072, contextWindowTokens: 204800, releasedAt: "2026-03-18" }, { id: "MiniMax-M2.5", maxOutput: 131072, contextWindowTokens: 204800, releasedAt: "2026-02-12" }, diff --git a/packages/core/src/llm/service-presets.ts b/packages/core/src/llm/service-presets.ts index a58ca5a5..038709cb 100644 --- a/packages/core/src/llm/service-presets.ts +++ b/packages/core/src/llm/service-presets.ts @@ -29,7 +29,7 @@ export const SERVICE_PRESETS: Record = { temperatureRange: [0, 2], defaultTemperature: 0.9, writingTemperature: 0.9, - knownModels: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1", "MiniMax-M2.1-highspeed", "MiniMax-M2"], + knownModels: ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1", "MiniMax-M2.1-highspeed", "MiniMax-M2"], }, bailian: { providerFamily: "anthropic", diff --git a/packages/studio/package.json b/packages/studio/package.json index 2b4d8678..0c5edfc5 100644 --- a/packages/studio/package.json +++ b/packages/studio/package.json @@ -1,6 +1,6 @@ { "name": "@actalk/inkos-studio", - "version": "1.6.2", + "version": "1.6.3", "description": "InkOS Studio — web workbench for story creation, InkOS Short, covers, and open-world interactive play", "type": "module", "main": "dist/api/index.js", @@ -31,7 +31,7 @@ "typecheck": "tsc --noEmit && tsc -p tsconfig.server.json --noEmit" }, "dependencies": { - "@actalk/inkos-core": "workspace:*", + "@actalk/inkos-core": "1.6.3", "@base-ui/react": "^1.3.0", "@fontsource-variable/geist": "^5.2.8", "@hono/node-server": "^1.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34348d44..17fdb5b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,10 @@ importers: packages/cli: dependencies: '@actalk/inkos-core': - specifier: workspace:* + specifier: 1.6.3 version: link:../core '@actalk/inkos-studio': - specifier: workspace:* + specifier: 1.6.3 version: link:../studio commander: specifier: ^13.0.0 @@ -107,7 +107,7 @@ importers: packages/studio: dependencies: '@actalk/inkos-core': - specifier: workspace:* + specifier: 1.6.3 version: link:../core '@base-ui/react': specifier: ^1.3.0 diff --git a/scripts/verify-no-workspace-protocol.mjs b/scripts/verify-no-workspace-protocol.mjs index 25d81183..38097397 100644 --- a/scripts/verify-no-workspace-protocol.mjs +++ b/scripts/verify-no-workspace-protocol.mjs @@ -1,15 +1,13 @@ /** - * Verify that source manifests are publishable once prepack normalization runs. + * Verify that source manifests are publishable as registry manifests. * * Usage: * node scripts/verify-no-workspace-protocol.mjs packages/cli packages/core * node ../../scripts/verify-no-workspace-protocol.mjs . * - * This script is safe to run on source manifests before prepack. - * * Checks two invariants before publish: - * 1. workspace:* / workspace:^ / workspace:~ references can be normalized to real versions - * 2. non-workspace internal dependencies already point at the current workspace version + * 1. dependencies / optionalDependencies / peerDependencies contain no workspace: specifiers + * 2. internal workspace dependencies point at the current workspace version */ import { access, readdir, readFile } from "node:fs/promises"; @@ -64,14 +62,6 @@ async function loadWorkspaceVersions(workspaceRoot) { return versions; } -function normalizeWorkspaceSpecifier(specifier, version) { - const value = specifier.slice("workspace:".length); - if (value === "*" || value === "") return version; - if (value === "^") return `^${version}`; - if (value === "~") return `~${version}`; - return value; -} - let failed = false; const workspaceRoot = await findWorkspaceRoot(process.cwd()); const workspaceVersions = await loadWorkspaceVersions(workspaceRoot); @@ -93,25 +83,11 @@ for (const dirArg of dirs) { } if (specifier.startsWith("workspace:")) { - if (!workspaceVersion) { - process.stderr.write(`FAIL: ${dir} — ${field}.${name}: ${specifier} (workspace package not found)\n`); - dirFailed = true; - failed = true; - continue; - } - - const normalized = normalizeWorkspaceSpecifier(specifier, workspaceVersion); - if ( - normalized !== workspaceVersion - && normalized !== `^${workspaceVersion}` - && normalized !== `~${workspaceVersion}` - ) { - process.stderr.write( - `FAIL: ${dir} — ${field}.${name}: ${specifier} normalizes to ${normalized}, expected ${workspaceVersion}, ^${workspaceVersion}, or ~${workspaceVersion}\n`, - ); - dirFailed = true; - failed = true; - } + process.stderr.write( + `FAIL: ${dir} — ${field}.${name}: ${specifier} (workspace protocol is not allowed in publish manifests)\n`, + ); + dirFailed = true; + failed = true; continue; }