Skip to content

Migrating from alpha.25 to alpha.26 ​

⚠️ BREAKING (upcoming in alpha.27). The instructions and prompt fields are deprecated in alpha.26 and will be removed in alpha.27 (~2 weeks after alpha.26 ships). This release adds the canonical messages: LLMMessage[] input alongside the deprecated fields; deprecation warnings emit when the legacy shape is used.

A one-line migration shim (toMessages) makes the mechanical upgrade take ~30 minutes for a 20-call-site consumer.

Install ​

bash
pnpm add @llm-ports/core@alpha @llm-ports/adapter-openai@alpha

All 7 publishable packages bumped to 0.1.0-alpha.26.

The headline ​

Every provider (OpenAI, Anthropic, Google, DeepInfra, Cerebras, Groq, SambaNova, everyone) speaks messages: Message[] natively. The port's { instructions, prompt } shape was a compression for the single-turn case that doesn't model multi-turn workloads (chat, interview agents, coaching workflows).

Alpha.26 unifies:

  • New: messages: LLMMessage[] on all four generation methods (generateText, generateStructured, streamText, streamStructured).
  • Deprecated: instructions?: string + prompt?: MessageContent on the same four methods.
  • Migration shim: toMessages(instructions, prompt) returns the equivalent messages array.
  • Convenience helpers: sys(content: string) and usr(content: MessageContent).
  • New errors: MessagesRequiredError, EmptyMessagesError, MessagesConflictError, PromptRequiredError.
  • Deprecation-warning UX: fingerprint-deduplicated console.warn per method per Registry, opt-out via suppressDeprecationWarnings: true, structured logging via deprecationWarningHandler.

runAgent is unchanged — it already took messages. Zero migration impact if you only use runAgent.

Migration paths ​

Path 1: Mechanical (one-line change per site, ~30 minutes for 20 sites) ​

Replace { instructions, prompt } with { messages: toMessages(instructions, prompt) }:

ts
// Before (alpha.25)
port.generateText({
  taskType: "triage",
  instructions: SYSTEM_PROMPT,
  prompt: userInput,
});

// After (alpha.26 mechanical, via shim)
import { toMessages } from "@llm-ports/core";

port.generateText({
  taskType: "triage",
  messages: toMessages(SYSTEM_PROMPT, userInput),
});

Same wire format, same behavior. This is the recommended first step — get the deprecation warnings out of your logs quickly, migrate to idiomatic on your own schedule.

Path 2: Idiomatic (uses sys / usr helpers) ​

Cleaner code, slightly more verbose than the shim:

ts
import { sys, usr } from "@llm-ports/core";

port.generateText({
  taskType: "triage",
  messages: [sys(SYSTEM_PROMPT), usr(userInput)],
});

sys() returns { role: "system", content: string }. usr() returns { role: "user", content: MessageContent } — accepts either a plain string or a MessageContent array for multimodal.

Path 3: Native multi-turn (previously unavailable) ​

The whole point of the unification — real multi-turn workloads with full conversation state:

ts
port.generateStructured({
  taskType: "interview-turn",
  schema: InterviewTurnSchema,
  messages: [
    sys(INTERVIEWER_SYSTEM_PROMPT),
    usr(student.firstAnswer),
    { role: "assistant", content: interviewer.firstFollowup },
    usr(student.secondAnswer),
    { role: "assistant", content: interviewer.secondFollowup },
    usr(student.thirdAnswer),
    // Model produces the next assistant turn
  ],
});

In alpha.25 this required rolling history into a string (loses role fidelity) or abusing runAgent with an empty tools object. Now it's a first-class shape.

What the Registry does under the hood ​

The RegistryPort normalizes both shapes before dispatch:

opts.messages is set?
├─ yes: was legacy also set?
│  ├─ yes → throw MessagesConflictError (ambiguity is a caller bug)
│  ├─ no  → messages length 0?
│  │       ├─ yes → throw EmptyMessagesError
│  │       └─ no  → dispatch with opts.messages
│  
└─ no: opts.prompt is set?
   ├─ yes → emit deprecation warning (deduped);
   │        synthesize messages = toMessages(opts.instructions, opts.prompt);
   │        dispatch with synthesized messages
   └─ no  → throw MessagesRequiredError

The adapter always sees options.messages after normalization — even when the caller used the legacy shape.

Deprecation warning UX ​

Format: single-line console.warn per method per Registry:

[llm-ports] DEPRECATED: 'instructions'/'prompt' fields on generateText will be removed in alpha.27. Use 'messages: LLMMessage[]' instead. See https://github.com/baabakk/llm-ports/blob/main/docs/migration/alpha-25-to-alpha-26.md.

Dedup: one warning per unique method per Registry instance. A consumer with 50 legacy call sites across all four methods gets 4 warnings total (one per method) — enough signal to trigger a migration audit without flooding logs.

Suppression (mid-migration opt-out):

ts
const registry = createRegistryFromEnv({
  // ...existing options...
  suppressDeprecationWarnings: true, // alpha.26+; removed in alpha.27
});

Structured logging (route warnings through your logger instead of console.warn):

ts
const registry = createRegistryFromEnv({
  // ...existing options...
  deprecationWarningHandler: (msg) => {
    logger.warn({ deprecation: true, msg });
  },
});

Both options ship in alpha.26 and are removed in alpha.27 alongside the legacy fields.

Error paths ​

Four new errors are exported from @llm-ports/core:

ErrorThrown when
MessagesRequiredErrorNeither messages nor prompt supplied on a call
EmptyMessagesErrormessages supplied but the array is empty
MessagesConflictErrorBoth messages AND legacy { instructions, prompt } supplied
PromptRequiredErrortoMessages() called with no prompt

All extend LLMPortError so blanket catch (e instanceof LLMPortError) handling continues to work.

Adapter behavior ​

adapter-openai reads options.messages directly when set (the Registry-normalized path). For the rare case of a direct-adapter call bypassing the Registry, the adapter falls back to { instructions, prompt }. Both shapes produce identical wire format for single-turn calls.

System-role message handling: the adapter extracts leading contiguous system-role messages from messages, concatenates them (with \n\n separator), and treats the concatenation as instructions for the underlying provider. Non-contiguous system messages (system in the middle of a conversation) pass through inline; OpenAI natively supports them as boundary markers.

Multi-system-role messages: contiguous system messages at the start are concatenated. Non-contiguous system messages pass through. If you need per-provider policy for these (Anthropic + Google split system into a top-level field and don't accept mid-conversation system messages), the adapters may throw provider-native errors in alpha.26; a follow-up patch release will normalize this cross-adapter.

Cache control: cacheControl on the call options continues to work unchanged. Anthropic's block-level cache_control markers on TextBlocks inside a messages array's content field are respected verbatim.

Multi-turn use cases now natively supported ​

  • Chat UIs — full conversation history passed each turn.
  • Interview / coaching workflows — rolling state where prior turns matter.
  • Iterative refinement — feedback loops with the user in the middle.
  • Roleplay / persona-based generation — arbitrary conversation state.
  • Any workflow that maintains chat state — no more providerExtras workarounds.

Timeline ​

MilestoneTarget
alpha.26 ships2026-07-02 (this release)
Migration window2026-07-02 to 2026-07-16
alpha.27 ships (removes deprecated fields)2026-07-16

Alpha.27 is the removal release. Consumers who haven't migrated by then hit TypeScript compilation errors on upgrade. Deprecation warnings during the alpha.26 window make the migration surface visible in advance.

Package versions ​

All 7 publishable packages bumped in lockstep:

  • @llm-ports/core@0.1.0-alpha.26
  • @llm-ports/adapter-openai@0.1.0-alpha.26
  • @llm-ports/adapter-anthropic@0.1.0-alpha.26
  • @llm-ports/adapter-google@0.1.0-alpha.26
  • @llm-ports/adapter-ollama@0.1.0-alpha.26
  • @llm-ports/adapter-vercel@0.1.0-alpha.26
  • @llm-ports/capabilities@0.1.0-alpha.26

Test coverage: 881 total (was 864 at alpha.25; +17 new for alpha.26 messages input; zero regressions).

Alternatives considered and rejected ​

  • Parallel field with XOR semantics (add messages alongside prompt, exactly one must be set forever). Rejected: two-ways-to-do-one-thing is a maintenance surface that never converges. The one-cycle deprecation window is the price of unifying.
  • New chat method (chat(opts: ChatOptions)). Rejected: grows the port from 5 methods to 7 for a semantic distinction that doesn't exist (chat and generateText produce identical output; only the input shape differs).
  • Rename prompt to input with union type (input: MessageContent | LLMMessage[]). Rejected: runtime discrimination churn without clarity gain.
  • Extend MessageContent to include LLMMessage[]. Rejected: collapses "content of a turn" and "sequence of turns" into one type.
  • Defer to beta.0. Considered seriously; rejected. Beta.0 is the stability signal — if we're breaking, we should break in alpha.
  • Do nothing. Rejected. The abstraction fails its job if it refuses to model a use case the protocol supports natively.

FAQ ​

Q: I can't upgrade all my code in one PR — can I migrate incrementally?

Yes. suppressDeprecationWarnings: true on the Registry silences warnings during your migration window. Land the migration in whatever cadence works; TypeScript compilation is your enforcement at alpha.27.

Q: What if I want per-turn refs in a multi-turn conversation?

Refs are per-call in alpha.26 — they apply to the whole call, not individual messages. If you want per-turn refs (unusual), split into multiple calls or embed the ref in your own message metadata outside refs.

Q: Can I still pass instructions alone with the new messages shape?

No — either use messages alone (put the system content as [sys(instructions), ...]) or use the deprecated { instructions, prompt } shape. Mixing throws MessagesConflictError.

Q: Does my adapter need to be updated for alpha.26?

If you're a consumer of @llm-ports/adapter-openai (or any of the bundled adapters), no. The alpha.26 ship updates all bundled adapters to read from options.messages. If you maintain a custom third-party adapter, update it to prefer options.messages when set; the Registry always passes it now.

Q: Do I need to update my capability-factory calls (createExtractor, createClassifier, etc.)?

No. The capability factories in @llm-ports/capabilities continue to accept the same task-specific inputs ({ input: string }, etc.). Their internal port calls have been updated to construct messages under the hood. Zero migration impact for capability-factory consumers.

Q: Where does runAgent fit?

runAgent already took messages and is unchanged. Alpha.26 aligns the other four methods with what runAgent already had.

Full test coverage summary ​

  • 10 helper + shim tests (toMessages, sys, usr, error paths)
  • 5 canonical messages-flow tests (Registry → adapter passing verbatim)
  • 4 legacy-path tests (deprecation warning fires, dedups, respects suppression)
  • 8 error-path tests (all four new errors)
  • All existing alpha.25 tests continue to pass unchanged

881 total tests pass across the workspace (was 864 at alpha.25; +17; zero regressions).

MIT License