diff --git a/packages/ai/src/host.ts b/packages/ai/src/host.ts index f7731f0f78a5..f7ee85619352 100644 --- a/packages/ai/src/host.ts +++ b/packages/ai/src/host.ts @@ -319,6 +319,11 @@ export function resolveAiTransportHeaderSentinels( const host = getAiTransportHost(); let resolvedHeaders: Record | undefined; for (const [name, value] of Object.entries(headers)) { + if (value === null) { + // applyLocalNoAuthHeaderOverride marks no-auth local providers with a + // runtime null marker outside the public string-only Model contract. + continue; + } const resolved = host.resolveSecretSentinel(value); if (resolved !== value) { resolvedHeaders ??= { ...headers }; diff --git a/packages/ai/src/transports/simple-completion-transport.test.ts b/packages/ai/src/transports/simple-completion-transport.test.ts index c10bbf1bb735..b1c781f0dd2d 100644 --- a/packages/ai/src/transports/simple-completion-transport.test.ts +++ b/packages/ai/src/transports/simple-completion-transport.test.ts @@ -239,7 +239,7 @@ describe("prepareModelForSimpleCompletion", () => { expect(result).toBe(model); }); - it("aliases a provider-owned stream when its wire-format api is already registered", async () => { + it("aliases a provider-owned stream while preserving its null auth-header marker", async () => { const builtInStream = vi.fn(() => createAssistantMessageEventStream()); apiRegistry.registerApiProvider( { @@ -260,6 +260,7 @@ describe("prepareModelForSimpleCompletion", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32768, maxTokens: 2048, + headers: { Authorization: null } as never, }; const result = prepareModelForSimpleCompletion({ model }); diff --git a/scripts/run-node-watch-paths.mts b/scripts/run-node-watch-paths.mts index e23ccb5fcb80..11cc66d566e3 100644 --- a/scripts/run-node-watch-paths.mts +++ b/scripts/run-node-watch-paths.mts @@ -10,9 +10,11 @@ const RUN_NODE_PACKAGE_SOURCE_ROOTS = [ // Root runtime code imports these package sources through tsconfig aliases, // while pnpm dev/watch still runs the root dist entrypoint. Treat them like // src/ so edits restart the same process that consumes them. + "packages/ai/src", "packages/gateway-client/src", "packages/gateway-protocol/src", "packages/markdown-core/src", + "packages/llm-core/src", "packages/media-core/src", "packages/media-generation-core/src", "packages/media-understanding-common/src", diff --git a/src/agents/failover/context-overflow.test.ts b/src/agents/failover/context-overflow.test.ts index 51f10f3b18ef..4bf926404de8 100644 --- a/src/agents/failover/context-overflow.test.ts +++ b/src/agents/failover/context-overflow.test.ts @@ -9,4 +9,12 @@ describe("isLikelyContextOverflowError", () => { ), ).toBe(true); }); + + it("does not mistake LM Studio prompt-template override guidance for overflow", () => { + expect( + isLikelyContextOverflowError( + 'Error rendering prompt with jinja template: "Cannot apply filter upper to type UndefinedValue". You can override the prompt template in model settings.', + ), + ).toBe(false); + }); }); diff --git a/src/agents/failover/context-overflow.ts b/src/agents/failover/context-overflow.ts index 2dc28b0b1762..49b5d7135413 100644 --- a/src/agents/failover/context-overflow.ts +++ b/src/agents/failover/context-overflow.ts @@ -133,6 +133,9 @@ export function isLikelyContextOverflowError(errorMessage?: string): boolean { if (isContextOverflowError(errorMessage)) { return true; } + if (normalizeLowercaseStringOrEmpty(errorMessage).includes("prompt template")) { + return false; + } if (RATE_LIMIT_HINT_RE.test(errorMessage)) { return false; } diff --git a/test/scripts/bundled-plugin-assets.test.ts b/test/scripts/bundled-plugin-assets.test.ts index 2f9b0fe3e74f..3213409b67fd 100644 --- a/test/scripts/bundled-plugin-assets.test.ts +++ b/test/scripts/bundled-plugin-assets.test.ts @@ -119,6 +119,14 @@ describe("bundled plugin assets", () => { expect(isRestartRelevantRunNodePath("extensions/discord/src/activities/http.ts")).toBe(true); }); + it.each(["packages/ai/src/host.ts", "packages/llm-core/src/types.ts"])( + "rebuilds the root runtime for %s", + (source) => { + expect(isBuildRelevantRunNodePath(source)).toBe(true); + expect(isRestartRelevantRunNodePath(source)).toBe(true); + }, + ); + it("refreshes generated output metadata without recreating the watcher", async () => { await withPluginAssetFixture(async (rootDir) => { const packagePath = path.join(rootDir, "extensions", "canvas", "package.json");