@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/.absolutejs/prettier.cache.json +9 -9
- package/.absolutejs/tsconfig.tsbuildinfo +1 -1
- package/.absolutejs/vue-tsc.tsbuildinfo +1 -1
- package/ROADMAP.md +0 -300
- package/dist/ai/index.js +17 -1
- package/dist/ai/index.js.map +4 -4
- package/dist/ai/providers/openaiResponses.js +17 -1
- package/dist/ai/providers/openaiResponses.js.map +3 -3
- package/dist/index.js +6095 -22
- package/dist/index.js.map +143 -4
- package/dist/src/ai/providers/openaiResponses.d.ts +3 -0
- package/dist/src/utils/defineEnv.d.ts +3 -0
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/types/env.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/types/env.ts +3 -0
- package/types/index.ts +1 -0
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=
|
|
1795
|
+
//# debugId=9D42156228088F4264756E2164756E21
|
|
1780
1796
|
//# sourceMappingURL=index.js.map
|