From c63c0e401a6e40f6f3f6fb430ec15087ba298e83 Mon Sep 17 00:00:00 2001 From: zrr1999 <2742392377@qq.com> Date: Thu, 30 Jul 2026 20:19:46 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ai):=20harden=20Pi=20compati?= =?UTF-8?q?bility=20loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- .../src/baidu-oneapi-compat-extension.test.ts | 62 ++++- .../src/baidu-oneapi-compat-extension.ts | 6 +- packages/spark-ai/src/baidu-oneapi.ts | 50 +++- pnpm-lock.yaml | 247 +++++++++++++++++- scripts/check-architecture-ratchets.mjs | 148 +++++++++-- test/architecture-ratchets.test.ts | 50 +++- test/pi-compatibility-loader.test.ts | 115 ++++++++ test/spark-cli.test.ts | 56 ---- 9 files changed, 634 insertions(+), 102 deletions(-) create mode 100644 test/pi-compatibility-loader.test.ts diff --git a/package.json b/package.json index e3641add..afd3ba66 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "typebox": "catalog:" }, "devDependencies": { + "@earendil-works/pi-coding-agent": "^0.82.1", "@j178/prek": "0.4.6", "@types/node": "^26.1.1", "@zendev-lab/spark-daemon-client": "workspace:*", @@ -91,7 +92,6 @@ "dependency-cruiser": "^18.1.0", "es-toolkit": "catalog:", "esbuild": "^0.28.0", - "jiti": "2.7.0", "jscpd": "^5.0.12", "knip": "^6.27.0", "p-retry": "catalog:", diff --git a/packages/spark-ai/src/baidu-oneapi-compat-extension.test.ts b/packages/spark-ai/src/baidu-oneapi-compat-extension.test.ts index c7904800..98cb04a6 100644 --- a/packages/spark-ai/src/baidu-oneapi-compat-extension.test.ts +++ b/packages/spark-ai/src/baidu-oneapi-compat-extension.test.ts @@ -10,7 +10,11 @@ import { expect, test } from "vitest"; import registerBaiduOneApiCompatibilityExtension from "./baidu-oneapi-compat-extension.ts"; import registerBaiduOneApiProvider from "./baidu-oneapi-provider.ts"; -import { type BaiduOneApiStream, createBaiduOneApiProviderAdapter } from "./baidu-oneapi.ts"; +import { + type BaiduOneApiStream, + createBaiduOneApiProviderAdapter, + silenceOpenAiSdkTransportLogs, +} from "./baidu-oneapi.ts"; import { SparkProviderRegistry } from "./provider-registry.ts"; const BAIDU_MODEL_IDS = [ @@ -95,6 +99,62 @@ test("Pi compatibility and Spark-native adapters expose the same Baidu model cat expect(piProvider?.api).toBe("baidu-oneapi"); }); +test("OpenAI SDK log suppression spans lazy setup and concurrent streams", async () => { + const previousOpenAiLog = process.env.OPENAI_LOG; + process.env.OPENAI_LOG = "debug"; + const first = Promise.withResolvers(); + const second = Promise.withResolvers(); + const third = Promise.withResolvers(); + const pending = [first.promise, second.promise, third.promise]; + const transport: ProviderStreams = { + stream: (model) => deferredResultStream(model, pending.shift()!), + streamSimple: (model) => deferredResultStream(model, pending.shift()!), + }; + const silenced = silenceOpenAiSdkTransportLogs(transport); + const model = testModel("gpt-5.6-sol"); + + try { + const firstStream = silenced.streamSimple(model, { messages: [], tools: [] }); + const secondStream = silenced.streamSimple(model, { messages: [], tools: [] }); + expect(process.env.OPENAI_LOG).toBe("off"); + + first.resolve(terminalMessage(model)); + await firstStream.result(); + await Promise.resolve(); + expect(process.env.OPENAI_LOG).toBe("off"); + + second.resolve(terminalMessage(model)); + await secondStream.result(); + await Promise.resolve(); + expect(process.env.OPENAI_LOG).toBe("debug"); + + const thirdStream = silenced.streamSimple(model, { messages: [], tools: [] }); + process.env.OPENAI_LOG = "info"; + third.resolve(terminalMessage(model)); + await thirdStream.result(); + await Promise.resolve(); + expect(process.env.OPENAI_LOG).toBe("info"); + } finally { + const terminal = terminalMessage(model); + first.resolve(terminal); + second.resolve(terminal); + third.resolve(terminal); + await Promise.allSettled([first.promise, second.promise, third.promise]); + if (previousOpenAiLog === undefined) delete process.env.OPENAI_LOG; + else process.env.OPENAI_LOG = previousOpenAiLog; + } +}); + +function deferredResultStream( + model: Model, + result: Promise, +): ReturnType { + return { + async *[Symbol.asyncIterator]() {}, + result: () => result, + } as unknown as ReturnType; +} + test("Baidu Responses uses one top-level instructions value and leaves caller overrides intact", async () => { const contexts: Context[] = []; const payloads: unknown[] = []; diff --git a/packages/spark-ai/src/baidu-oneapi-compat-extension.ts b/packages/spark-ai/src/baidu-oneapi-compat-extension.ts index 9ecf62c6..be1c28b2 100644 --- a/packages/spark-ai/src/baidu-oneapi-compat-extension.ts +++ b/packages/spark-ai/src/baidu-oneapi-compat-extension.ts @@ -1,12 +1,10 @@ -import { anthropicMessagesApi, lazyApi } from "@earendil-works/pi-ai/compat"; +import { anthropicMessagesApi, openAIResponsesApi } from "@earendil-works/pi-ai/compat"; import { createBaiduOneApiProviderAdapter, silenceOpenAiSdkTransportLogs } from "./baidu-oneapi.ts"; const piBaiduOneApiProvider = createBaiduOneApiProviderAdapter({ anthropicMessages: anthropicMessagesApi(), - openAIResponses: lazyApi(async () => - silenceOpenAiSdkTransportLogs(await import("@earendil-works/pi-ai/api/openai-responses")), - ), + openAIResponses: silenceOpenAiSdkTransportLogs(openAIResponsesApi()), }); export default function registerBaiduOneApiCompatibilityExtension( diff --git a/packages/spark-ai/src/baidu-oneapi.ts b/packages/spark-ai/src/baidu-oneapi.ts index d4e47ab2..705fbf11 100644 --- a/packages/spark-ai/src/baidu-oneapi.ts +++ b/packages/spark-ai/src/baidu-oneapi.ts @@ -99,6 +99,11 @@ const CLAUDE_OPUS_COST = { cacheWrite: 6.875, }; +// The compat factory imports the transport after stream() returns, so the process-wide +// OpenAI log guard must remain active until the lazy stream reaches a terminal result. +let openAiSdkLogGuardDepth = 0; +let openAiSdkPreviousLogLevel: string | undefined; + export function silenceOpenAiSdkTransportLogs(transport: ProviderStreams): ProviderStreams { return { stream: (model, context, options) => @@ -108,16 +113,45 @@ export function silenceOpenAiSdkTransportLogs(transport: ProviderStreams): Provi }; } -function withOpenAiSdkLoggingDisabled(start: () => T): T { - const previous = process.env.OPENAI_LOG; - process.env.OPENAI_LOG = "off"; +function withOpenAiSdkLoggingDisabled(start: () => T): T { + const release = acquireOpenAiSdkLogGuard(); + let stream: T; + try { + stream = start(); + } catch (error) { + release(); + throw error; + } try { - // pi-ai creates the OpenAI client synchronously before returning its event stream. - return start(); - } finally { - if (previous === undefined) delete process.env.OPENAI_LOG; - else process.env.OPENAI_LOG = previous; + void stream.result().then(release, release); + } catch (error) { + release(); + throw error; } + return stream; +} + +function acquireOpenAiSdkLogGuard(): () => void { + if (openAiSdkLogGuardDepth === 0) { + openAiSdkPreviousLogLevel = process.env.OPENAI_LOG; + process.env.OPENAI_LOG = "off"; + } + openAiSdkLogGuardDepth += 1; + let released = false; + return () => { + if (released) return; + released = true; + openAiSdkLogGuardDepth -= 1; + if (openAiSdkLogGuardDepth > 0) return; + if (process.env.OPENAI_LOG === "off") { + if (openAiSdkPreviousLogLevel === undefined) { + Reflect.deleteProperty(process.env, "OPENAI_LOG"); + } else { + process.env.OPENAI_LOG = openAiSdkPreviousLogLevel; + } + } + openAiSdkPreviousLogLevel = undefined; + }; } function mapThinkingEffort( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88677d33..507f677c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: specifier: 'catalog:' version: 1.3.6 devDependencies: + '@earendil-works/pi-coding-agent': + specifier: ^0.82.1 + version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3) '@j178/prek': specifier: 0.4.6 version: 0.4.6 @@ -166,9 +169,6 @@ importers: esbuild: specifier: ^0.28.0 version: 0.28.1 - jiti: - specifier: 2.7.0 - version: 2.7.0 jscpd: specifier: ^5.0.12 version: 5.0.12 @@ -1913,11 +1913,20 @@ packages: resolution: {integrity: sha512-VIh8oW89XXACUkQqB2N8TaU3A/y2jQieF++VC6QqIqKhKRKj7kd+pzeh43MXusuPpebtxtEzqvjaCLlc1xbYTQ==} engines: {node: '>=22.13'} + '@earendil-works/pi-agent-core@0.82.1': + resolution: {integrity: sha512-Z3kloziJIE2dmrisRckZX8zDca/gIv9/YdFAzeoqpHiLV2wsni6bL4hInNSjVKLbqT+4kqLIkph2JQLKvSepjg==} + engines: {node: '>=22.19.0'} + '@earendil-works/pi-ai@0.82.1': resolution: {integrity: sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==} engines: {node: '>=22.19.0'} hasBin: true + '@earendil-works/pi-coding-agent@0.82.1': + resolution: {integrity: sha512-zbkAhoIuDPMF3pKuja0ajZabrMWU29FUMV9A/XMXT/XC1yXs5xt6t6t13GogQFsDrDqbFP4DkZQO1w8rWRAzYA==} + engines: {node: '>=22.19.0'} + hasBin: true + '@earendil-works/pi-tui@0.82.1': resolution: {integrity: sha512-9yN8hALfKaxZq7n54EMxqhFCWnMi6LHkraMJ/1YjHiATq75XrI6XDMVppn9EDtiK7Fks8hUe1SDXUTrIvwRWfQ==} engines: {node: '>=22.19.0'} @@ -2799,6 +2808,74 @@ packages: peerDependencies: svelte: ^5 + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -3712,6 +3789,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + '@sinclair/typebox@0.31.17': resolution: {integrity: sha512-GVYVHHOGINx+DT2DwjXoCQO0mRpztYKyb3d48tucuqhjhHpQYGp7Xx7dhhQGzILx/beuBrzfITMC7/5X7fw+UA==} @@ -5459,6 +5539,10 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -5564,10 +5648,17 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.12.31: resolution: {integrity: sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==} engines: {node: '>=16.9.0'} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -5646,6 +5737,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + ignore@7.0.6: resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} @@ -6327,6 +6422,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -6578,6 +6677,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -6678,6 +6781,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} @@ -6857,6 +6963,10 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -6999,6 +7109,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -7320,6 +7433,10 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -8690,6 +8807,21 @@ snapshots: '@cursor/sdk-linux-x64': 1.0.23 '@cursor/sdk-win32-x64': 1.0.23 + '@earendil-works/pi-agent-core@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3) + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) @@ -8711,6 +8843,36 @@ snapshots: - ws - zod + '@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3) + '@earendil-works/pi-tui': 0.82.1 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.1.38 + undici: 8.5.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-tui@0.82.1': dependencies: get-east-asian-width: 1.6.0 @@ -9394,6 +9556,50 @@ snapshots: dependencies: svelte: 5.56.7 + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -10080,6 +10286,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@silvia-odwyer/photon-node@0.3.4': {} + '@sinclair/typebox@0.31.17': {} '@sindresorhus/is@7.2.0': {} @@ -11982,6 +12190,12 @@ snapshots: github-slugger@2.0.0: {} + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -12220,8 +12434,14 @@ snapshots: property-information: 7.2.0 space-separated-tokens: 2.0.2 + highlight.js@10.7.3: {} + hono@4.12.31: {} + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.2 + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.15.1 @@ -12312,6 +12532,8 @@ snapshots: ieee754@1.2.1: {} + ignore@7.0.5: {} + ignore@7.0.6: {} import-fresh@3.3.0: @@ -13204,6 +13426,8 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.3: {} + mkdirp-classic@0.5.3: {} mri@1.2.0: {} @@ -13505,6 +13729,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -13595,6 +13824,12 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@7.2.0: {} protobufjs@7.6.5: @@ -13860,6 +14095,8 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + retry@0.12.0: {} + retry@0.13.1: {} robust-predicates@3.0.3: {} @@ -14133,6 +14370,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -14452,6 +14691,8 @@ snapshots: undici@7.28.0: {} + undici@8.5.0: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 diff --git a/scripts/check-architecture-ratchets.mjs b/scripts/check-architecture-ratchets.mjs index 3e91f394..b5147613 100644 --- a/scripts/check-architecture-ratchets.mjs +++ b/scripts/check-architecture-ratchets.mjs @@ -1,6 +1,6 @@ import { readFileSync, readdirSync, statSync } from "node:fs"; -import { dirname, extname, join, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { dirname, extname, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import ts from "typescript"; const root = dirname(dirname(fileURLToPath(import.meta.url))); @@ -175,13 +175,10 @@ function runArchitectureRatchets() { } const extensionPath = join(root, extension); if (!isFile(extensionPath)) continue; - const unsafePiImports = findUnsafePiCompatibilityImports( - readFileSync(extensionPath, "utf8"), - extension, - ); + const unsafePiImports = findUnsafePiCompatibilityImportsInGraph(extensionPath); if (unsafePiImports.length > 0) { failures.push( - `${extension} statically imports Pi subpaths unsupported by the compatibility loader (${unsafePiImports.join(", ")}). Use the virtualized package root and defer modern public subpaths behind a Spark-owned compatibility adapter.`, + `${extension} runtime graph imports Pi subpaths unsupported by the compatibility loader (${unsafePiImports.join(", ")}). Use only loader-virtualized Pi entries from compatibility extensions.`, ); } } @@ -244,15 +241,13 @@ export function findUnsafePiCompatibilityImports(source, fileName = "source.ts") const unsafe = new Set(); function inspect(node) { + const specifier = moduleSpecifierText(node); if ( - (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && - node.moduleSpecifier && - ts.isStringLiteralLike(node.moduleSpecifier) + specifier === "@mariozechner/pi-ai" || + specifier?.startsWith("@mariozechner/pi-ai/") || + (specifier?.startsWith("@earendil-works/pi-ai/") && !safeSpecifiers.has(specifier)) ) { - const specifier = node.moduleSpecifier.text; - if (specifier.startsWith("@earendil-works/pi-ai/") && !safeSpecifiers.has(specifier)) { - unsafe.add(specifier); - } + unsafe.add(specifier); } ts.forEachChild(node, inspect); } @@ -260,6 +255,113 @@ export function findUnsafePiCompatibilityImports(source, fileName = "source.ts") return [...unsafe].sort((left, right) => left.localeCompare(right)); } +export function findUnsafePiCompatibilityImportsInGraph(entryPath) { + const pending = [entryPath]; + const visited = new Set(); + const unsafe = new Set(); + + while (pending.length > 0) { + const currentPath = pending.pop(); + if (!currentPath || visited.has(currentPath) || !isFile(currentPath)) continue; + visited.add(currentPath); + const source = readFileSync(currentPath, "utf8"); + const displayPath = relative(root, currentPath); + for (const specifier of findUnsafePiCompatibilityImports(source, displayPath)) { + unsafe.add(`${displayPath}: ${specifier}`); + } + for (const specifier of findRuntimeModuleSpecifiers(source, displayPath)) { + const resolved = resolveCompatibilitySourceModule(currentPath, specifier); + if (resolved) pending.push(resolved); + } + } + + return [...unsafe].sort((left, right) => left.localeCompare(right)); +} + +function findRuntimeModuleSpecifiers(source, fileName) { + const sourceFile = ts.createSourceFile( + fileName, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + const specifiers = new Set(); + function inspect(node) { + const specifier = moduleSpecifierText(node); + if (specifier) specifiers.add(specifier); + ts.forEachChild(node, inspect); + } + inspect(sourceFile); + return [...specifiers]; +} + +function moduleSpecifierText(node) { + if ( + (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && + node.moduleSpecifier && + ts.isStringLiteralLike(node.moduleSpecifier) && + !isTypeOnlyModuleEdge(node) + ) { + return node.moduleSpecifier.text; + } + if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments.length === 1 && + ts.isStringLiteralLike(node.arguments[0]) + ) { + return node.arguments[0].text; + } + return undefined; +} + +function isTypeOnlyModuleEdge(node) { + if (ts.isExportDeclaration(node)) return node.isTypeOnly; + const importClause = node.importClause; + if (!importClause) return false; + if (importClause.isTypeOnly) return true; + if (importClause.name || !importClause.namedBindings) return false; + return ( + ts.isNamedImports(importClause.namedBindings) && + importClause.namedBindings.elements.every((element) => element.isTypeOnly) + ); +} + +function resolveCompatibilitySourceModule(importerPath, specifier) { + if (specifier.startsWith(".")) return resolveRelativeSourceModule(importerPath, specifier); + if (!specifier.startsWith("@zendev-lab/")) return undefined; + try { + const resolvedPath = fileURLToPath( + import.meta.resolve(specifier, pathToFileURL(join(root, "package.json"))), + ); + if (!resolvedPath.startsWith(`${root}${sep}`) || !isFile(resolvedPath)) return undefined; + return resolvedPath; + } catch { + return undefined; + } +} + +function resolveRelativeSourceModule(importerPath, specifier) { + const base = resolve(dirname(importerPath), specifier); + const candidates = extname(base) + ? [base] + : [ + base, + `${base}.ts`, + `${base}.tsx`, + `${base}.mts`, + `${base}.js`, + `${base}.mjs`, + join(base, "index.ts"), + join(base, "index.tsx"), + join(base, "index.mts"), + join(base, "index.js"), + join(base, "index.mjs"), + ]; + return candidates.find((candidate) => isFile(candidate)); +} + export function findLegacyDaemonClientViolations(source, fileName = "source.ts") { const sourceFile = ts.createSourceFile( fileName, @@ -272,17 +374,7 @@ export function findLegacyDaemonClientViolations(source, fileName = "source.ts") let usesLegacyRequestSymbol = false; function inspect(node) { - const moduleSpecifier = - (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && - node.moduleSpecifier && - ts.isStringLiteralLike(node.moduleSpecifier) - ? node.moduleSpecifier.text - : ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length === 1 && - ts.isStringLiteralLike(node.arguments[0]) - ? node.arguments[0].text - : undefined; + const moduleSpecifier = moduleSpecifierText(node); if (moduleSpecifier === "@zendev-lab/spark-daemon-client/local-rpc") { importsLegacySubpath = true; } @@ -347,7 +439,11 @@ function isFile(path) { } function readJson(path) { - return JSON.parse(readFileSync(path, "utf8")); + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch (error) { + throw new Error(`Failed to parse JSON from ${path}`, { cause: error }); + } } if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { diff --git a/test/architecture-ratchets.test.ts b/test/architecture-ratchets.test.ts index 13f268e7..202fb1dd 100644 --- a/test/architecture-ratchets.test.ts +++ b/test/architecture-ratchets.test.ts @@ -1,3 +1,7 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, relative } from "node:path"; + import { describe, expect, it } from "vitest"; // @ts-expect-error The executable architecture script intentionally has no declaration surface. @@ -6,6 +10,7 @@ import * as architectureRatchets from "../scripts/check-architecture-ratchets.mj const { findLegacyDaemonClientViolations, findUnsafePiCompatibilityImports, + findUnsafePiCompatibilityImportsInGraph, isLegacyDaemonClientBoundaryExempt, } = architectureRatchets; @@ -63,27 +68,66 @@ describe("legacy daemon client architecture ratchet", () => { }); describe("Pi compatibility extension architecture ratchet", () => { - it("rejects static imports that the compatibility loader rewrites as root-prefixed paths", () => { + it("rejects static and dynamic imports that the compatibility loader rewrites as root-prefixed paths", () => { expect( findUnsafePiCompatibilityImports(` import * as piAi from "@earendil-works/pi-ai"; import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy"; export { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex"; + import legacyPiAi from "@mariozechner/pi-ai"; + piAi.lazyApi(() => import("@earendil-works/pi-ai/api/openai-responses")); `), ).toEqual([ "@earendil-works/pi-ai/api/anthropic-messages.lazy", + "@earendil-works/pi-ai/api/openai-responses", "@earendil-works/pi-ai/providers/openai-codex", + "@mariozechner/pi-ai", ]); }); - it("allows virtualized static entries and deferred modern public subpaths", () => { + it("allows only Pi entries virtualized by the production compatibility loader", () => { expect( findUnsafePiCompatibilityImports(` import * as piAi from "@earendil-works/pi-ai"; import { getModels } from "@earendil-works/pi-ai/compat"; import type { OAuthProviderInterface } from "@earendil-works/pi-ai/oauth"; - piAi.lazyApi(() => import("@earendil-works/pi-ai/api/openai-responses")); + import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all"; `), ).toEqual([]); }); + + it("follows workspace package exports from a compatibility entrypoint", async () => { + const dir = await mkdtemp(join(tmpdir(), "spark-pi-workspace-import-ratchet-")); + try { + const entry = join(dir, "extension.ts"); + await writeFile(entry, 'import "@zendev-lab/spark-ai/baidu-oneapi-provider";\n', "utf8"); + + expect(findUnsafePiCompatibilityImportsInGraph(entry)).toEqual([ + "packages/spark-ai/src/baidu-oneapi-provider.ts: @earendil-works/pi-ai/api/anthropic-messages.lazy", + "packages/spark-ai/src/baidu-oneapi-provider.ts: @earendil-works/pi-ai/api/openai-responses", + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it("follows relative imports from a compatibility entrypoint", async () => { + const dir = await mkdtemp(join(tmpdir(), "spark-pi-import-ratchet-")); + try { + const entry = join(dir, "extension.ts"); + const helper = join(dir, "helper.ts"); + await writeFile(entry, 'import "./helper.ts";\n', "utf8"); + await writeFile( + helper, + 'await import("@earendil-works/pi-ai/api/openai-responses");\n', + "utf8", + ); + + expect(findUnsafePiCompatibilityImportsInGraph(entry)).toEqual([ + `${relative(process.cwd(), helper)}: @earendil-works/pi-ai/api/openai-responses`, + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/test/pi-compatibility-loader.test.ts b/test/pi-compatibility-loader.test.ts new file mode 100644 index 00000000..fe4d30b5 --- /dev/null +++ b/test/pi-compatibility-loader.test.ts @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent"; +import { test } from "vitest"; + +const COMPATIBILITY_EXTENSION = resolve("packages/spark-ai/src/baidu-oneapi-compat-extension.ts"); + +test("the production Pi loader executes both Baidu OneAPI lazy transports", async () => { + const agentDir = await mkdtemp(join(tmpdir(), "spark-pi-loader-")); + const previousBaiduApiKey = process.env.BAIDU_ONEAPI_API_KEY; + const previousOpenAiLog = process.env.OPENAI_LOG; + const previousFetch = globalThis.fetch; + delete process.env.BAIDU_ONEAPI_API_KEY; + process.env.OPENAI_LOG = "debug"; + let requests = 0; + let openAiLogDuringRequest: string | undefined; + globalThis.fetch = (async () => { + requests += 1; + openAiLogDuringRequest = process.env.OPENAI_LOG; + return completedResponsesResponse(); + }) as typeof fetch; + + try { + const loaded = await discoverAndLoadExtensions( + [COMPATIBILITY_EXTENSION], + process.cwd(), + agentDir, + ); + assert.deepEqual(loaded.errors, []); + + const registrations = loaded.runtime.pendingProviderRegistrations.filter( + (registration) => registration.name === "baidu-oneapi", + ); + assert.equal(registrations.length, 1); + const config = registrations[0]?.config; + assert.ok(config?.streamSimple); + + const claude = modelFromConfig(config, "claude-opus-5"); + const claudeStream = config.streamSimple( + claude, + { messages: [], tools: [] }, + { apiKey: "", maxRetries: 0, maxRetryDelayMs: 1 }, + ); + for await (const _event of claudeStream) void _event; + const claudeResult = await claudeStream.result(); + assert.equal(claudeResult.stopReason, "error"); + assert.match(claudeResult.errorMessage ?? "", /No API key for provider: baidu-oneapi/u); + + const gpt = modelFromConfig(config, "gpt-5.6-sol"); + const gptStream = config.streamSimple( + gpt, + { messages: [], tools: [] }, + { apiKey: "test-key", maxRetries: 0, maxRetryDelayMs: 1 }, + ); + for await (const _event of gptStream) void _event; + const gptResult = await gptStream.result(); + + assert.equal(gptResult.stopReason, "stop"); + assert.equal(gptResult.api, "baidu-oneapi"); + assert.equal(gptResult.provider, "baidu-oneapi"); + assert.equal(requests, 1); + assert.equal(openAiLogDuringRequest, "off"); + assert.equal(process.env.OPENAI_LOG, "debug"); + assert.doesNotMatch(gptResult.errorMessage ?? "", /compat\.js\/api|Cannot find module/u); + } finally { + globalThis.fetch = previousFetch; + if (previousBaiduApiKey === undefined) delete process.env.BAIDU_ONEAPI_API_KEY; + else process.env.BAIDU_ONEAPI_API_KEY = previousBaiduApiKey; + if (previousOpenAiLog === undefined) delete process.env.OPENAI_LOG; + else process.env.OPENAI_LOG = previousOpenAiLog; + await rm(agentDir, { recursive: true, force: true }); + } +}); + +function modelFromConfig( + config: NonNullable< + Awaited< + ReturnType + >["runtime"]["pendingProviderRegistrations"][number] + >["config"], + modelId: string, +) { + const definition = config.models?.find((model) => model.id === modelId); + assert.ok(definition, `missing ${modelId} from the compatibility provider`); + return { + ...definition, + api: "baidu-oneapi" as const, + provider: "baidu-oneapi", + baseUrl: definition.baseUrl ?? config.baseUrl ?? "https://oneapi-comate.baidu-int.com", + }; +} + +function completedResponsesResponse(): Response { + return new Response( + `data: ${JSON.stringify({ + type: "response.completed", + response: { + id: "resp_loader_contract", + status: "completed", + output: [], + usage: { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + input_tokens_details: { cached_tokens: 0 }, + output_tokens_details: { reasoning_tokens: 0 }, + }, + }, + })}\n\n`, + { status: 200, headers: { "content-type": "text/event-stream" } }, + ); +} diff --git a/test/spark-cli.test.ts b/test/spark-cli.test.ts index eb18cc8c..c9630717 100644 --- a/test/spark-cli.test.ts +++ b/test/spark-cli.test.ts @@ -1,7 +1,4 @@ import assert from "node:assert/strict"; -import { realpathSync } from "node:fs"; -import { join } from "node:path"; -import { createJiti } from "jiti"; import { test } from "vitest"; import { @@ -457,59 +454,6 @@ test("Baidu OneAPI stream wrapper normalizes done error and result paths for bot } }); -test("Baidu OneAPI lazy adapters load through the Pi compatibility Jiti graph", async () => { - const piAiRoot = realpathSync( - join(process.cwd(), "packages/spark-ai/node_modules/@earendil-works/pi-ai"), - ); - const jiti = createJiti(import.meta.url, { - moduleCache: false, - alias: { - "@earendil-works/pi-ai": join(piAiRoot, "dist/compat.js"), - "@earendil-works/pi-ai/api/anthropic-messages.lazy": join( - piAiRoot, - "dist/api/anthropic-messages.lazy.js", - ), - "@earendil-works/pi-ai/api/openai-responses": join(piAiRoot, "dist/api/openai-responses.js"), - }, - }); - const provider = (await jiti.import( - join(process.cwd(), "packages/spark-ai/src/baidu-oneapi-provider.ts"), - )) as typeof import("../packages/spark-ai/src/baidu-oneapi-provider.ts"); - const context = { messages: [], tools: [] }; - const baseModel = { - name: "Jiti compatibility adapter", - api: "baidu-oneapi", - provider: "baidu-oneapi", - baseUrl: "https://oneapi-comate.baidu-int.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1000, - maxTokens: 1000, - }; - const streams = [ - provider.streamBaiduOneApiAnthropic( - { ...baseModel, id: "claude-opus-5" } as never, - context as never, - { apiKey: "" }, - ), - provider.streamBaiduOneApiOpenAIResponses( - { ...baseModel, id: "gpt-5.6-luna", baseUrl: baseModel.baseUrl + "/v1" } as never, - context as never, - { apiKey: "" }, - ), - ]; - for (const stream of streams) { - for await (const _event of stream) void _event; - const result = await stream.result(); - assert.equal(result.api, "baidu-oneapi"); - assert.equal(result.provider, "baidu-oneapi"); - assert.equal(result.stopReason, "error"); - assert.match(result.errorMessage ?? "", /No API key for provider: baidu-oneapi/); - assert.doesNotMatch(result.errorMessage ?? "", /Mismatched api/); - } -}); - test("Baidu OneAPI adapters use upstream transport APIs but report baidu-oneapi", async () => { const context = { messages: [], tools: [] }; const baseModel = {