@absolutejs/absolute 0.19.0-beta.264 → 0.19.0-beta.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ROADMAP.md CHANGED
@@ -512,306 +512,6 @@ Elysia POST handlers work for form processing, but there's no convention for pro
512
512
 
513
513
  ---
514
514
 
515
- ## 7. P1 — AI/LLM Streaming Helpers
516
-
517
- **What exists today in the ecosystem:**
518
- Vercel's `ai` SDK (`npm install ai`) provides React hooks and server utilities for streaming LLM responses. It works with Next.js, SvelteKit, and Nuxt. But it uses Server-Sent Events (SSE) because Next.js has no native WebSocket support. SSE is one-directional (server → client only) — the client can't send messages mid-stream (cancel, follow-up, branch conversation) without opening a new HTTP request.
519
-
520
- **What AbsoluteJS has today:**
521
- Native bidirectional WebSocket via Elysia. The HMR system already proves the WebSocket infrastructure works at scale with reconnection, message typing, and broadcast. But there are no helpers for connecting an LLM API to a WebSocket channel.
522
-
523
- **Why this is a killer feature:**
524
- AI-powered apps are the dominant use case for new web projects in 2025-2026. Every chat interface, AI assistant, code editor, and content generator needs LLM streaming. The current state of the art (Vercel AI SDK over SSE) has real limitations:
525
- - SSE is one-directional — canceling a generation requires a separate abort request
526
- - SSE connections can't be reused — each message opens a new HTTP connection
527
- - No native support for branching conversations, tool use feedback, or multi-turn streaming
528
- - WebSocket solves all of these: bidirectional, persistent, multiplexed
529
-
530
- AbsoluteJS has WebSocket built in. Adding thin helpers on top makes it the best framework for AI apps with zero additional infrastructure.
531
-
532
- **What needs to be built:**
533
-
534
- *Server side — `streamAI()` utility:*
535
- ```ts
536
- import { streamAI } from 'absolutejs'
537
-
538
- app.ws('/chat', {
539
- message: async (ws, { prompt, conversationId }) => {
540
- // streamAI connects to the LLM provider, pipes token chunks
541
- // over the WebSocket, and handles backpressure/cancellation
542
- await streamAI(ws, {
543
- provider: 'anthropic',
544
- model: 'claude-sonnet-4-5-20250514',
545
- messages: [{ role: 'user', content: prompt }],
546
- // Optional: called for each token chunk before sending
547
- onChunk: (chunk) => {
548
- // Transform, log, filter, or store chunks
549
- saveToConversation(conversationId, chunk)
550
- return chunk
551
- },
552
- // Optional: called when the stream completes
553
- onComplete: (fullResponse) => {
554
- saveMessage(conversationId, fullResponse)
555
- }
556
- })
557
- }
558
- })
559
- ```
560
-
561
- - `streamAI()` is provider-agnostic — supports Anthropic, OpenAI, and any provider that returns a `ReadableStream` or async iterator
562
- - Handles backpressure — if the client is slow to consume, the server buffers appropriately
563
- - Handles cancellation — if the client disconnects or sends a cancel message, the LLM request is aborted
564
- - Handles errors — if the LLM API errors mid-stream, sends a typed error message to the client
565
- - The `onChunk` callback enables middleware-like processing: content filtering, token counting, database persistence, RAG augmentation
566
-
567
- *Provider adapters:*
568
- ```ts
569
- // Built-in adapters for common providers
570
- import { anthropic, openai, ollama } from 'absolutejs/ai'
571
-
572
- // Each adapter normalizes the provider's streaming API into a common interface
573
- await streamAI(ws, {
574
- provider: anthropic({ apiKey: env.ANTHROPIC_API_KEY }),
575
- // or: provider: openai({ apiKey: env.OPENAI_API_KEY }),
576
- // or: provider: ollama({ baseUrl: 'http://localhost:11434' }),
577
- model: 'claude-sonnet-4-5-20250514',
578
- messages,
579
- })
580
- ```
581
-
582
- - Adapters handle provider-specific auth, endpoints, and stream formats
583
- - Common interface: `{ stream: AsyncIterable<{ type: 'text' | 'tool_use' | 'error', content: string }> }`
584
- - Easy to add new providers — just implement the adapter interface
585
-
586
- *Client side — per-framework hooks:*
587
-
588
- **React:**
589
- ```tsx
590
- import { useAIStream } from 'absolutejs/react'
591
-
592
- const Chat = () => {
593
- const {
594
- messages, // Message[] — full conversation history
595
- send, // (content: string) => void — send a message
596
- cancel, // () => void — cancel current generation
597
- isStreaming, // boolean — true while LLM is generating
598
- error, // string | null — last error
599
- } = useAIStream('/chat')
600
-
601
- return (
602
- <div>
603
- {messages.map(m => (
604
- <div key={m.id} data-role={m.role}>
605
- {m.content}
606
- {m.isStreaming && <span className="cursor" />}
607
- </div>
608
- ))}
609
- <form onSubmit={(e) => {
610
- e.preventDefault()
611
- send(e.currentTarget.input.value)
612
- }}>
613
- <input name="input" disabled={isStreaming} />
614
- {isStreaming
615
- ? <button type="button" onClick={cancel}>Stop</button>
616
- : <button type="submit">Send</button>
617
- }
618
- </form>
619
- </div>
620
- )
621
- }
622
- ```
623
-
624
- **Svelte:**
625
- ```svelte
626
- <script lang="ts">
627
- import { createAIStream } from 'absolutejs/svelte'
628
-
629
- const { messages, send, cancel, isStreaming, error } = createAIStream('/chat')
630
- </script>
631
-
632
- {#each $messages as message}
633
- <div data-role={message.role}>
634
- {message.content}
635
- {#if message.isStreaming}<span class="cursor" />{/if}
636
- </div>
637
- {/each}
638
-
639
- <form on:submit|preventDefault={(e) => send(e.currentTarget.input.value)}>
640
- <input name="input" disabled={$isStreaming} />
641
- {#if $isStreaming}
642
- <button type="button" on:click={cancel}>Stop</button>
643
- {:else}
644
- <button type="submit">Send</button>
645
- {/if}
646
- </form>
647
- ```
648
-
649
- **Vue:**
650
- ```vue
651
- <script setup lang="ts">
652
- import { useAIStream } from 'absolutejs/vue'
653
-
654
- const { messages, send, cancel, isStreaming, error } = useAIStream('/chat')
655
- </script>
656
-
657
- <template>
658
- <div v-for="m in messages" :key="m.id" :data-role="m.role">
659
- {{ m.content }}
660
- <span v-if="m.isStreaming" class="cursor" />
661
- </div>
662
- <form @submit.prevent="send($event.target.input.value)">
663
- <input name="input" :disabled="isStreaming" />
664
- <button v-if="isStreaming" type="button" @click="cancel">Stop</button>
665
- <button v-else type="submit">Send</button>
666
- </form>
667
- </template>
668
- ```
669
-
670
- **Angular:**
671
- ```typescript
672
- import { Component, inject } from '@angular/core'
673
- import { AIStreamService } from 'absolutejs/angular'
674
-
675
- @Component({
676
- selector: 'app-chat',
677
- template: `
678
- @for (m of ai.messages(); track m.id) {
679
- <div [attr.data-role]="m.role">
680
- {{ m.content }}
681
- @if (m.isStreaming) { <span class="cursor"></span> }
682
- </div>
683
- }
684
- <form (submit)="onSubmit($event)">
685
- <input name="input" [disabled]="ai.isStreaming()" />
686
- @if (ai.isStreaming()) {
687
- <button type="button" (click)="ai.cancel()">Stop</button>
688
- } @else {
689
- <button type="submit">Send</button>
690
- }
691
- </form>
692
- `
693
- })
694
- export class ChatComponent {
695
- ai = inject(AIStreamService).connect('/chat')
696
-
697
- onSubmit(e: Event) {
698
- e.preventDefault()
699
- const input = (e.target as HTMLFormElement).input as HTMLInputElement
700
- this.ai.send(input.value)
701
- }
702
- }
703
- ```
704
-
705
- *WebSocket message protocol for AI streaming:*
706
- ```ts
707
- // Client → Server
708
- type AIClientMessage =
709
- | { type: 'message', content: string, conversationId?: string }
710
- | { type: 'cancel' }
711
-
712
- // Server → Client
713
- type AIServerMessage =
714
- | { type: 'chunk', content: string, messageId: string }
715
- | { type: 'tool_use', name: string, input: unknown, messageId: string }
716
- | { type: 'complete', messageId: string, usage?: { inputTokens: number, outputTokens: number } }
717
- | { type: 'error', message: string }
718
- ```
719
-
720
- *Advanced features:*
721
- - **Tool use / function calling**: When the LLM calls a tool, the server sends a `tool_use` message. The client can display a "searching..." or "running code..." UI. The server executes the tool and feeds the result back to the LLM, continuing the stream.
722
- - **Conversation branching**: The client sends a `conversationId` — the server maintains conversation state and supports branching (edit a previous message, regenerate from a point).
723
- - **Multi-model routing**: `streamAI` could accept a `router` function that picks the model based on the message (simple questions → Haiku, complex → Opus).
724
- - **Token counting**: The `onChunk` callback can count tokens for usage tracking / rate limiting.
725
- - **Reconnection**: If the WebSocket drops mid-stream, the client hook reconnects and requests the remaining content from the server (using the `messageId` as a cursor).
726
-
727
- **Design considerations:**
728
- - The AI helpers should be optional — users who don't build AI features never import them and they're tree-shaken from the bundle.
729
- - Provider API keys should come from environment variables, never sent to the client.
730
- - The client hooks manage the WebSocket connection lifecycle — connect on mount, disconnect on unmount, reconnect on drop. Same patterns as the HMR client.
731
- - The message protocol should be extensible — providers may add new message types (images, audio) and the protocol should handle unknown types gracefully.
732
- - SSR: the chat component renders empty on the server (no messages). Conversation history loads on hydration from the server or local storage.
733
-
734
- **Files likely involved:**
735
- - New: `src/ai/streamAI.ts` — core streaming utility that pipes LLM responses to WebSocket
736
- - New: `src/ai/providers/anthropic.ts` — Anthropic adapter
737
- - New: `src/ai/providers/openai.ts` — OpenAI adapter
738
- - New: `src/ai/providers/ollama.ts` — Ollama adapter (local LLMs)
739
- - New: `src/react/hooks/useAIStream.ts` — React hook
740
- - New: `src/svelte/createAIStream.ts` — Svelte store-based hook
741
- - New: `src/vue/useAIStream.ts` — Vue composable
742
- - New: `src/angular/ai-stream.service.ts` — Angular service
743
- - New: `types/ai.ts` — message protocol types, provider interface, hook return types
744
- - Package exports: `absolutejs/ai` for server utilities, per-framework exports for client hooks
745
-
746
- ---
747
-
748
- ## 8. P1 — Type-Safe Environment Variables (`defineEnv`)
749
-
750
- **The problem:**
751
- `process.env.DATABASE_URL` is always `string | undefined` in TypeScript. Typos are silent (`process.env.DATABSE_URL` → `undefined`, no error). Missing vars cause runtime crashes in production. Numeric vars like `PORT` come back as strings and need manual parsing. There's no single place to see what env vars an app requires.
752
-
753
- **What AbsoluteJS has today:**
754
- `getEnv(key)` — a runtime helper that throws if the variable is missing. But it's one-variable-at-a-time, returns `string` always, and provides no type narrowing or compile-time safety.
755
-
756
- **What needs to be built:**
757
-
758
- ```ts
759
- // env.ts — the single source of truth for all environment variables
760
- import { defineEnv } from 'absolutejs'
761
- import { t } from 'elysia'
762
-
763
- export const env = defineEnv({
764
- DATABASE_URL: t.String({ format: 'url' }),
765
- PORT: t.Number({ default: 3000 }),
766
- NODE_ENV: t.Union([t.Literal('development'), t.Literal('production')]),
767
- STRIPE_SECRET: t.String(),
768
- ENABLE_LOGGING: t.Boolean({ default: true }),
769
- MAX_UPLOAD_MB: t.Number({ default: 10 }),
770
- })
771
- ```
772
-
773
- **How `defineEnv` works:**
774
- - Called once at app startup (in `server.ts` or imported by `prepare()`)
775
- - Reads all declared vars from `process.env` / `Bun.env`
776
- - Validates each var against its schema using Elysia's `t` (TypeBox) — same type system used everywhere else in AbsoluteJS
777
- - Parses types: `"3000"` → `3000` for numbers, `"true"` → `true` for booleans
778
- - Applies defaults for missing optional vars
779
- - **Fails fast** on startup if any required var is missing or fails validation — clear error message listing every problem:
780
- ```
781
- Environment validation failed:
782
- ✗ DATABASE_URL — required but not set
783
- ✗ PORT — expected number, got "not-a-number"
784
- ✓ NODE_ENV — "production"
785
- ✓ STRIPE_SECRET — set
786
- ```
787
- - Returns a fully typed, frozen object — `env.PORT` is `number`, `env.NODE_ENV` is `'development' | 'production'`, `env.TYPO` is a TypeScript error
788
-
789
- **Usage throughout the app:**
790
- ```ts
791
- // server.ts
792
- import { env } from './env'
793
-
794
- app.listen(env.PORT) // number, not string
795
-
796
- // any file
797
- import { env } from './env'
798
- if (env.NODE_ENV === 'development') { ... } // narrowed union
799
- ```
800
-
801
- **Design considerations:**
802
- - Uses Elysia's `t` (TypeBox) for schemas — same system developers already use for route validation. No new schema library to learn.
803
- - The returned `env` object is `Object.freeze()`'d — env vars don't change at runtime.
804
- - Sensitive vars (containing `SECRET`, `KEY`, `TOKEN`, `PASSWORD` in the name) are redacted in error messages and logs — `"STRIPE_SECRET — set"` not `"STRIPE_SECRET — sk_live_..."`.
805
- - `.env` file support via Bun's built-in dotenv — `defineEnv` validates after Bun loads the `.env` file.
806
- - Warn if a `.env` file contains vars with `SECRET`/`KEY` in the name and the file isn't in `.gitignore`.
807
-
808
- **Files likely involved:**
809
- - New: `src/utils/defineEnv.ts` — the `defineEnv` function with TypeBox validation, parsing, and error formatting
810
- - New: `types/env.ts` — type utilities for extracting the typed env object from a schema
811
- - `src/utils/getEnv.ts` — deprecate in favor of `defineEnv`, or keep as a lightweight alternative for simple cases
812
-
813
- ---
814
-
815
515
  ## 9. P1 — Security Headers + CSP Nonce Injection
816
516
 
817
517
  **The problem:**
package/dist/ai/index.js CHANGED
@@ -588,6 +588,12 @@ var buildRequestBody2 = (params, isImageModel) => {
588
588
  if (tools) {
589
589
  body.tools = tools;
590
590
  }
591
+ if (params.thinking) {
592
+ body.reasoning = {
593
+ effort: "high",
594
+ summary: "auto"
595
+ };
596
+ }
591
597
  return body;
592
598
  };
593
599
  var parseJSON = (data) => {
@@ -719,6 +725,16 @@ var processCompleted = function* (parsed) {
719
725
  };
720
726
  var processSSEEvent = function* (eventType, parsed, pendingCalls) {
721
727
  switch (eventType) {
728
+ case "response.reasoning_summary_text.delta": {
729
+ const delta = typeof parsed.delta === "string" ? parsed.delta : "";
730
+ if (delta) {
731
+ yield {
732
+ content: delta,
733
+ type: "thinking"
734
+ };
735
+ }
736
+ break;
737
+ }
722
738
  case "response.output_text.delta":
723
739
  yield* processTextDelta(parsed);
724
740
  break;
@@ -1776,5 +1792,5 @@ export {
1776
1792
  aiChat
1777
1793
  };
1778
1794
 
1779
- //# debugId=B9E25972E76C002A64756E2164756E21
1795
+ //# debugId=9D42156228088F4264756E2164756E21
1780
1796
  //# sourceMappingURL=index.js.map