Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
5ad7d425 address PR #575 review feedback
Maps to tombeckenham's review items C1, I1–I6:
- C1 (runtime validation): `normalizeSystemPrompts` throws TypeError naming
the offending index when an object-form entry's content isn't a string,
so stale call sites can't stream a literal "undefined" into the model.
New test file `packages/typescript/ai/tests/system-prompts.test.ts`
covers the happy paths and the throw cases.
- I1 (E2E coverage): `testing/e2e/tests/system-prompt-metadata.spec.ts`
drives the full Anthropic HTTP path with object-form systemPrompts +
metadata.cache_control. Wire-shape assertion stays in the ai-anthropic
unit test (aimock's journal normalises Anthropic into an OpenAI-shaped
request and drops `cache_control`, so a journal-based assertion isn't
reliable — the spec asserts end-to-end success instead). Required a
test-only `systemPromptCacheControl` opt-in on `api.chat.ts` to flip
the system prompt to object form.
- I2 (middleware widened-shape coverage): new
`middleware.test.ts > should preserve object-form systemPrompts through
middleware` asserts the middleware sees `Array<SystemPrompt>`, not a
pre-flattened `Array<string>`, and that mutations preserve metadata
through to the adapter.
- I3 (per-adapter mapping coverage): direct unit tests for
Gemini (`systemInstruction` joined; foreign metadata dropped),
Ollama (`messages.unshift({role:'system'})` joined; foreign metadata
dropped), and OpenRouter (positional `{role:'system'}` message; foreign
metadata dropped).
- I4 (OTel metadata observability): when `captureContent: true` and at
least one entry carries metadata, the iteration span gains a
`tanstack.ai.system_prompt.metadata` JSON attribute (positional
per-prompt array, `null` for plain strings / no metadata). Kept off
span events so the existing one-event-per-message GenAI semconv stays
intact. New tests in `tests/middlewares/otel.test.ts` cover both the
set and the absent path.
- I5 (doc/changeset wording): replaced "silently ignore" with the
accurate "carries no meaningful value … silently dropped, never
written to the wire" framing in `SystemPrompt`'s JSDoc, the
`TextOptions.systemPrompts` JSDoc, and the changeset. Removed the
misleading `satisfies AnthropicSystemPromptMetadata` from the
`@example` (the adapter narrows the metadata field at the call site,
so a `satisfies` cast is contradicted by the tests).
- I6 (devtools structural guard):
`ai-event-client/tests/devtools-middleware-shape.test.ts` re-declares
the local `DevtoolsSystemPrompt` mirror and uses `expectTypeOf` to
assert mutual assignability with `@tanstack/ai`'s `SystemPrompt`. If
the canonical shape gains a third variant the guard breaks at
type-check time, forcing the maintainer to update the mirror in
`devtools-middleware.ts`.
Suggestions S1–S5 deferred per the reviewer's "follow-up acceptable" call. 79f2b941 feat(ai): systemPrompts accept { content, metadata } with adapter-inferred metadata typing
Extends `chat({ systemPrompts })` to accept either a plain string (existing
shape — backward compatible) or `{ content, metadata }`. The structured
form's `metadata` type is inferred from the adapter at the chat() call site
via a new `TSystemPromptMetadata` generic on `TextAdapter` — no `satisfies`
needed by callers.
- Anthropic declares `AnthropicSystemPromptMetadata` → users get
`cache_control` autocomplete + type-checking.
- Adapters with no per-prompt metadata (OpenAI, Gemini, Ollama,
OpenRouter, openai-base) inherit the default `never`, which makes the
`metadata` field unusable at the call site. Passing metadata to those
adapters is a TypeScript error.
Closes the regression introduced by removing the `modelOptions.system`
escape hatch in the audit PR — there is now a public, typed path for
attaching Anthropic `cache_control` to system prompts.
Plumbing
- New `TSystemPromptMetadata = never` generic on `TextAdapter` /
`BaseTextAdapter`, surfaced via `'~types'['systemPromptMetadata']`.
`TextActivityOptions.systemPrompts` is now
`Array<SystemPrompt<TAdapter['~types']['systemPromptMetadata']>>`.
- `AnyTextAdapter` extended to 7 generic slots.
- `TextOptions.systemPrompts` (the wide internal shape adapters receive)
is `Array<SystemPrompt>`; adapters call `normalizeSystemPrompts<...>()`
to narrow.
- Chat engine + middleware context/config widened to carry
`Array<SystemPrompt>` so metadata flows through to the adapter.
- OpenTelemetry middleware extracts `.content` for span events;
per-prompt metadata is dropped from spans.
- `@tanstack/ai-event-client` mirrors the `SystemPrompt` shape locally
(avoids a circular import with `@tanstack/ai`) and projects metadata
away on the devtools wire.
Adapter mappings
- Anthropic reads `metadata.cache_control` and attaches it to the
matching `TextBlockParam`.
- OpenAI / Gemini / Ollama / OpenRouter / openai-base call
`normalizeSystemPrompts()` and join `.content` for their respective
`instructions` / `system` / `systemInstruction` fields. (Their
metadata type is `never`, so the field can't be set anyway.)
Tests
- ai-anthropic: new test verifies `cache_control` flows from
`systemPrompts[i].metadata` onto the outbound `TextBlockParam`, and
plain-string entries still produce metadata-less blocks.
- ai-openai: new test verifies mixed string + object-form input
(without metadata) produces the expected joined `instructions`.