@omg-dev/ai 0.4.24
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/dist/index.mjs +51 -0
- package/dist/react.mjs +2 -0
- package/package.json +43 -0
- package/src/index.ts +122 -0
- package/src/react.ts +27 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
|
+
import { convertToModelMessages, generateObject, generateText, stepCountIs, streamObject, streamText, tool } from "ai";
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const endUserStore = new AsyncLocalStorage();
|
|
7
|
+
/**
|
|
8
|
+
* Run `fn` with the given end-user identifier in scope. Any LLM call made
|
|
9
|
+
* (synchronously or asynchronously, transitively) inside `fn` will have
|
|
10
|
+
* `X-OMG-User: <userId>` stamped on the upstream request unless an
|
|
11
|
+
* explicit `headers` argument overrides it.
|
|
12
|
+
*
|
|
13
|
+
* Pass an empty/undefined userId to opt out (calls inside will be
|
|
14
|
+
* anonymous / unattributed).
|
|
15
|
+
*/
|
|
16
|
+
function runWithEndUser(userId, fn) {
|
|
17
|
+
if (!userId) return fn();
|
|
18
|
+
return endUserStore.run(userId, fn);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns the end-user id currently in scope, or `undefined` if no
|
|
22
|
+
* `runWithEndUser` is on the stack.
|
|
23
|
+
*/
|
|
24
|
+
function getEndUser() {
|
|
25
|
+
return endUserStore.getStore();
|
|
26
|
+
}
|
|
27
|
+
const attributedFetch = async (input, init) => {
|
|
28
|
+
const userId = endUserStore.getStore();
|
|
29
|
+
if (!userId) return fetch(input, init);
|
|
30
|
+
const headers = new Headers(init?.headers);
|
|
31
|
+
if (!headers.has("x-omg-user")) headers.set("X-OMG-User", userId);
|
|
32
|
+
return fetch(input, {
|
|
33
|
+
...init,
|
|
34
|
+
headers
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const ANTHROPIC_BASE = process.env.ANTHROPIC_BASE_URL || void 0;
|
|
38
|
+
const OPENAI_BASE = process.env.OPENAI_BASE_URL || void 0;
|
|
39
|
+
const fetchForSdk = attributedFetch;
|
|
40
|
+
const anthropic = createAnthropic({
|
|
41
|
+
baseURL: ANTHROPIC_BASE,
|
|
42
|
+
apiKey: process.env.ANTHROPIC_API_KEY || "omg-sandbox",
|
|
43
|
+
fetch: fetchForSdk
|
|
44
|
+
});
|
|
45
|
+
const openai = createOpenAI({
|
|
46
|
+
baseURL: OPENAI_BASE,
|
|
47
|
+
apiKey: process.env.OPENAI_API_KEY || "omg-sandbox",
|
|
48
|
+
fetch: fetchForSdk
|
|
49
|
+
});
|
|
50
|
+
//#endregion
|
|
51
|
+
export { anthropic, convertToModelMessages, generateObject, generateText, getEndUser, openai, runWithEndUser, stepCountIs, streamObject, streamText, tool };
|
package/dist/react.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omg-dev/ai",
|
|
3
|
+
"version": "0.4.24",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"default": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./react": {
|
|
11
|
+
"types": "./src/react.ts",
|
|
12
|
+
"default": "./dist/react.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ai": "^6.0.0",
|
|
17
|
+
"@ai-sdk/anthropic": "^3.0.0",
|
|
18
|
+
"@ai-sdk/openai": "^2.0.0",
|
|
19
|
+
"@ai-sdk/react": "^3.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18 || ^19"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"react": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/BennyKok/vibes.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://docs.omg.dev",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @omg-dev/ai — zero-config AI SDK (`ai` package) for omg-deployed apps.
|
|
2
|
+
//
|
|
3
|
+
// What this gives you:
|
|
4
|
+
// - `anthropic` and `openai` providers pre-pointed at the per-VM omg LLM
|
|
5
|
+
// proxy. No baseURL, no apiKey from your code.
|
|
6
|
+
// - `runWithEndUser(userId, fn)` — wraps any async handler so that LLM
|
|
7
|
+
// calls inside auto-stamp `X-OMG-User: <userId>` on the upstream request.
|
|
8
|
+
// Used by @omg-dev/auth's middleware to attribute every call to the
|
|
9
|
+
// end-user who triggered the request, with no per-call wiring.
|
|
10
|
+
//
|
|
11
|
+
// How attribution flows:
|
|
12
|
+
// 1. End-user request hits your server.
|
|
13
|
+
// 2. @omg-dev/auth middleware verifies their session, extracts userId.
|
|
14
|
+
// 3. Middleware calls `runWithEndUser(userId, () => handler(req))`.
|
|
15
|
+
// 4. Inside the handler, `streamText({ model: anthropic('...'), ... })`
|
|
16
|
+
// runs. The provider's fetch interceptor reads the ALS context and
|
|
17
|
+
// adds `X-OMG-User: <userId>` to the upstream call.
|
|
18
|
+
// 5. The omg LLM proxy reads that header and writes user_id into
|
|
19
|
+
// usage_logs / usage_aggregates.
|
|
20
|
+
//
|
|
21
|
+
// The header argument on individual `streamText` / `generateText` calls
|
|
22
|
+
// remains an escape hatch — if you pass `headers: { 'X-OMG-User': '...' }`
|
|
23
|
+
// explicitly, that wins over the ALS context. Useful for bots, cron jobs,
|
|
24
|
+
// or labeling on behalf of a user other than the request subject.
|
|
25
|
+
|
|
26
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
27
|
+
import { createAnthropic } from "@ai-sdk/anthropic"
|
|
28
|
+
import { createOpenAI } from "@ai-sdk/openai"
|
|
29
|
+
|
|
30
|
+
// ── End-user context ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const endUserStore = new AsyncLocalStorage<string>()
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run `fn` with the given end-user identifier in scope. Any LLM call made
|
|
36
|
+
* (synchronously or asynchronously, transitively) inside `fn` will have
|
|
37
|
+
* `X-OMG-User: <userId>` stamped on the upstream request unless an
|
|
38
|
+
* explicit `headers` argument overrides it.
|
|
39
|
+
*
|
|
40
|
+
* Pass an empty/undefined userId to opt out (calls inside will be
|
|
41
|
+
* anonymous / unattributed).
|
|
42
|
+
*/
|
|
43
|
+
export function runWithEndUser<T>(
|
|
44
|
+
userId: string | undefined | null,
|
|
45
|
+
fn: () => T | Promise<T>,
|
|
46
|
+
): T | Promise<T> {
|
|
47
|
+
if (!userId) return fn()
|
|
48
|
+
return endUserStore.run(userId, fn)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the end-user id currently in scope, or `undefined` if no
|
|
53
|
+
* `runWithEndUser` is on the stack.
|
|
54
|
+
*/
|
|
55
|
+
export function getEndUser(): string | undefined {
|
|
56
|
+
return endUserStore.getStore()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Fetch interceptor ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
// Wraps the global fetch so every upstream LLM call carries the end-user
|
|
62
|
+
// from ALS. The provider SDKs accept a `fetch` option; we hand them this.
|
|
63
|
+
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
64
|
+
|
|
65
|
+
const attributedFetch: FetchFn = async (input, init) => {
|
|
66
|
+
const userId = endUserStore.getStore()
|
|
67
|
+
if (!userId) return fetch(input, init)
|
|
68
|
+
|
|
69
|
+
// If caller already set X-OMG-User explicitly, leave it alone.
|
|
70
|
+
const headers = new Headers(init?.headers)
|
|
71
|
+
if (!headers.has("x-omg-user")) {
|
|
72
|
+
headers.set("X-OMG-User", userId)
|
|
73
|
+
}
|
|
74
|
+
return fetch(input, { ...init, headers })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Providers ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
// Read base URLs from env vars injected by the omg infra orchestrator. Falls
|
|
80
|
+
// back to the AI SDK defaults if the env isn't set (e.g. running locally
|
|
81
|
+
// outside an omg sandbox), so this package is safe to import anywhere.
|
|
82
|
+
const ANTHROPIC_BASE = process.env.ANTHROPIC_BASE_URL || undefined
|
|
83
|
+
const OPENAI_BASE = process.env.OPENAI_BASE_URL || undefined
|
|
84
|
+
|
|
85
|
+
// The AI SDK's `fetch` option types as `typeof fetch`, which (in newer
|
|
86
|
+
// Node/TS lib versions) includes `preconnect` — never actually called by the
|
|
87
|
+
// SDK. Cast at the boundary instead of polyfilling something we don't need.
|
|
88
|
+
const fetchForSdk = attributedFetch as unknown as typeof fetch
|
|
89
|
+
|
|
90
|
+
export const anthropic = createAnthropic({
|
|
91
|
+
baseURL: ANTHROPIC_BASE,
|
|
92
|
+
apiKey: process.env.ANTHROPIC_API_KEY || "omg-sandbox",
|
|
93
|
+
fetch: fetchForSdk,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
export const openai = createOpenAI({
|
|
97
|
+
baseURL: OPENAI_BASE,
|
|
98
|
+
apiKey: process.env.OPENAI_API_KEY || "omg-sandbox",
|
|
99
|
+
fetch: fetchForSdk,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── Re-exports ──────────────────────────────────────────────────────────────
|
|
103
|
+
// Re-export the pieces of `ai` that 80%+ of apps use directly. Importing
|
|
104
|
+
// from `@omg-dev/ai` becomes the one-line setup; no separate `import { ... }
|
|
105
|
+
// from "ai"` is needed for common cases.
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
streamText,
|
|
109
|
+
generateText,
|
|
110
|
+
streamObject,
|
|
111
|
+
generateObject,
|
|
112
|
+
tool,
|
|
113
|
+
stepCountIs,
|
|
114
|
+
// Convert UIMessage[] from the client (useChat sends `parts`-shaped
|
|
115
|
+
// messages) into the ModelMessage[] streamText accepts. Always run on
|
|
116
|
+
// the server side of a useChat-based app — passing UIMessages
|
|
117
|
+
// directly to streamText silently produces empty completions.
|
|
118
|
+
convertToModelMessages,
|
|
119
|
+
type ModelMessage,
|
|
120
|
+
type UIMessage,
|
|
121
|
+
type Tool,
|
|
122
|
+
} from "ai"
|
package/src/react.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Browser-safe entry — React hooks for chat / completion UIs.
|
|
2
|
+
//
|
|
3
|
+
// `@omg-dev/ai` (the root entry) is server-only because it imports
|
|
4
|
+
// `node:async_hooks` for the `runWithEndUser` ALS attribution. Browser
|
|
5
|
+
// bundlers (Vite, esbuild) refuse to bundle Node built-ins, so any
|
|
6
|
+
// component that imports from `@omg-dev/ai` directly fails to build.
|
|
7
|
+
//
|
|
8
|
+
// This subpath re-exports just the browser-safe surface from
|
|
9
|
+
// `@ai-sdk/react` — no Node imports, no fetch interceptor, no ALS. The
|
|
10
|
+
// hooks talk to your `functions/api/<name>.ts` routes via plain HTTP.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// import { useChat } from "@omg-dev/ai/react"
|
|
14
|
+
//
|
|
15
|
+
// Server side keeps using `from "@omg-dev/ai"` for the proxy-bound
|
|
16
|
+
// providers + ALS. The two halves never load each other; bundlers stay
|
|
17
|
+
// happy in both environments.
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
useChat,
|
|
21
|
+
useCompletion,
|
|
22
|
+
// The useObject hook is experimental in @ai-sdk/react@3 and ships under
|
|
23
|
+
// `experimental_useObject`. Re-export under the documented name so app
|
|
24
|
+
// code can stay future-proof when it stabilizes.
|
|
25
|
+
experimental_useObject as useObject,
|
|
26
|
+
type UseChatOptions,
|
|
27
|
+
} from "@ai-sdk/react"
|