@nixxie-cms/ai 1.0.0
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/LICENSE +23 -0
- package/README.md +41 -0
- package/dist/nixxie-cms-ai.cjs.d.ts +2 -0
- package/dist/nixxie-cms-ai.cjs.js +16 -0
- package/package.json +37 -0
- package/src/AnthropicProvider.ts +115 -0
- package/src/OpenAiProvider.ts +86 -0
- package/src/index.ts +26 -0
- package/src/types.ts +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nixxie International DMCC
|
|
4
|
+
Portions Copyright (c) 2023 Thinkmill Labs Pty Ltd and contributors
|
|
5
|
+
(this software is derived from the KeystoneJS project, https://keystonejs.com)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @nixxie-cms/ai
|
|
2
|
+
|
|
3
|
+
LLM text generation and embeddings for Nixxie CMS, with a provider-agnostic interface. Defaults to
|
|
4
|
+
Anthropic's Claude (model `claude-opus-4-8`) via the official `@anthropic-ai/sdk`; OpenAI is also
|
|
5
|
+
supported. SDKs are loaded lazily — install only the provider you use.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { config } from '@nixxie-cms/core'
|
|
9
|
+
import { createAi } from '@nixxie-cms/ai'
|
|
10
|
+
|
|
11
|
+
export default config({
|
|
12
|
+
ai: createAi({ provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY! }),
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Available everywhere as `context.services.ai`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// One-shot completion — great for generating SEO descriptions, summaries, alt text, etc.
|
|
20
|
+
const { text } = await context.services.ai.complete(
|
|
21
|
+
`Write a 155-character meta description for: ${post.title}`
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Multi-turn chat
|
|
25
|
+
const reply = await context.services.ai.chat([
|
|
26
|
+
{ role: 'system', content: 'You are a helpful editorial assistant.' },
|
|
27
|
+
{ role: 'user', content: 'Suggest five headline variations for this article.' },
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
// Embeddings — pair with @nixxie-cms/search for semantic search
|
|
31
|
+
const vector = await context.services.ai.embed(post.body)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Providers
|
|
35
|
+
|
|
36
|
+
| provider | SDK | embeddings |
|
|
37
|
+
| ----------- | ------------------ | ---------- |
|
|
38
|
+
| `anthropic` | `@anthropic-ai/sdk` | via Voyage AI (`voyageApiKey`) — Anthropic has no native embeddings endpoint |
|
|
39
|
+
| `openai` | `openai` | native (`text-embedding-3-small` by default) |
|
|
40
|
+
|
|
41
|
+
Install the SDK for your chosen provider, e.g. `npm install @anthropic-ai/sdk`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// this file might look strange and you might be wondering what it's for
|
|
3
|
+
// it's lets you import your source files by importing this entrypoint
|
|
4
|
+
// as you would import it if it was built with preconstruct build
|
|
5
|
+
// this file is slightly different to some others though
|
|
6
|
+
// it has a require hook which compiles your code with Babel
|
|
7
|
+
// this means that you don't have to set up @babel/register or anything like that
|
|
8
|
+
// but you can still require this module and it'll be compiled
|
|
9
|
+
|
|
10
|
+
// this bit of code imports the require hook and registers it
|
|
11
|
+
let unregister = require("../../../node_modules/.pnpm/@preconstruct+hook@0.4.0/node_modules/@preconstruct/hook").___internalHook(typeof __dirname === 'undefined' ? undefined : __dirname, "../../..", "..");
|
|
12
|
+
|
|
13
|
+
// this re-exports the source file
|
|
14
|
+
module.exports = require("../src/index.ts");
|
|
15
|
+
|
|
16
|
+
unregister();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nixxie-cms/ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "dist/nixxie-cms-ai.cjs.js",
|
|
6
|
+
"module": "dist/nixxie-cms-ai.esm.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/nixxie-cms-ai.cjs.js",
|
|
10
|
+
"module": "./dist/nixxie-cms-ai.esm.js",
|
|
11
|
+
"default": "./dist/nixxie-cms-ai.cjs.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@babel/runtime": "^7.24.7"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@nixxie-cms/core": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@nixxie-cms/core": "^1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"@anthropic-ai/sdk": "^0.69.0",
|
|
26
|
+
"openai": "^6.0.0"
|
|
27
|
+
},
|
|
28
|
+
"preconstruct": {
|
|
29
|
+
"entrypoints": [
|
|
30
|
+
"index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/nixxiecms/nixxie/tree/main/packages/ai"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NixxieAiCompleteOptions,
|
|
3
|
+
NixxieAiCompletion,
|
|
4
|
+
NixxieAiMessage,
|
|
5
|
+
NixxieAiService,
|
|
6
|
+
} from '@nixxie-cms/core'
|
|
7
|
+
import type { AiConfig } from './types'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MODEL = 'claude-opus-4-8'
|
|
10
|
+
|
|
11
|
+
function loadAnthropic(): any {
|
|
12
|
+
try {
|
|
13
|
+
return require('@anthropic-ai/sdk').default ?? require('@anthropic-ai/sdk')
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'The Anthropic provider requires @anthropic-ai/sdk. Run: npm install @anthropic-ai/sdk'
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Anthropic (Claude) provider, backed by the official @anthropic-ai/sdk. */
|
|
22
|
+
export class AnthropicProvider implements NixxieAiService {
|
|
23
|
+
private config: AiConfig
|
|
24
|
+
private model: string
|
|
25
|
+
private maxTokens: number
|
|
26
|
+
private client: any
|
|
27
|
+
|
|
28
|
+
constructor(config: AiConfig) {
|
|
29
|
+
this.config = config
|
|
30
|
+
this.model = config.model ?? DEFAULT_MODEL
|
|
31
|
+
this.maxTokens = config.maxTokens ?? 4096
|
|
32
|
+
const Anthropic = loadAnthropic()
|
|
33
|
+
this.client = new Anthropic({ apiKey: config.apiKey, baseURL: config.baseUrl })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async complete(prompt: string, options?: NixxieAiCompleteOptions): Promise<NixxieAiCompletion> {
|
|
37
|
+
return this.chat([{ role: 'user', content: prompt }], options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async chat(
|
|
41
|
+
messages: NixxieAiMessage[],
|
|
42
|
+
options?: NixxieAiCompleteOptions
|
|
43
|
+
): Promise<NixxieAiCompletion> {
|
|
44
|
+
// Anthropic takes the system prompt as a top-level field, not a message.
|
|
45
|
+
const system = [
|
|
46
|
+
options?.system,
|
|
47
|
+
...messages.filter(m => m.role === 'system').map(m => m.content),
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join('\n\n')
|
|
51
|
+
|
|
52
|
+
const turns = messages
|
|
53
|
+
.filter(m => m.role !== 'system')
|
|
54
|
+
.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }))
|
|
55
|
+
|
|
56
|
+
const model = options?.model ?? this.model
|
|
57
|
+
const response = await this.client.messages.create({
|
|
58
|
+
model,
|
|
59
|
+
max_tokens: options?.maxTokens ?? this.maxTokens,
|
|
60
|
+
system: system || undefined,
|
|
61
|
+
messages: turns,
|
|
62
|
+
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const text = (response.content ?? [])
|
|
66
|
+
.filter((block: any) => block.type === 'text')
|
|
67
|
+
.map((block: any) => block.text)
|
|
68
|
+
.join('')
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
text,
|
|
72
|
+
model: response.model ?? model,
|
|
73
|
+
usage: {
|
|
74
|
+
inputTokens: response.usage?.input_tokens,
|
|
75
|
+
outputTokens: response.usage?.output_tokens,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async embed(text: string): Promise<number[]> {
|
|
81
|
+
const [vector] = await this.embedMany([text])
|
|
82
|
+
if (!vector) throw new Error('Embedding provider returned no vector')
|
|
83
|
+
return vector
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async embedMany(texts: string[]): Promise<number[][]> {
|
|
87
|
+
if (!this.config.voyageApiKey) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'Anthropic has no native embeddings endpoint. Set `voyageApiKey` to generate embeddings via Voyage AI, or use the OpenAI provider.'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
const model = this.config.embeddingModel ?? 'voyage-3'
|
|
93
|
+
const res = await fetch('https://api.voyageai.com/v1/embeddings', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
Authorization: `Bearer ${this.config.voyageApiKey}`,
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({ input: texts, model }),
|
|
100
|
+
})
|
|
101
|
+
if (!res.ok) throw new Error(`Voyage embeddings failed (${res.status}): ${await res.text()}`)
|
|
102
|
+
const data: any = await res.json()
|
|
103
|
+
const items = (data.data ?? []) as Array<{ index?: number; embedding: number[] }>
|
|
104
|
+
if (items.length !== texts.length) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Voyage returned ${items.length} embeddings for ${texts.length} inputs`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
// The API does not guarantee response order matches input order — sort by `index`.
|
|
110
|
+
return items
|
|
111
|
+
.slice()
|
|
112
|
+
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
113
|
+
.map(d => d.embedding)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NixxieAiCompleteOptions,
|
|
3
|
+
NixxieAiCompletion,
|
|
4
|
+
NixxieAiMessage,
|
|
5
|
+
NixxieAiService,
|
|
6
|
+
} from '@nixxie-cms/core'
|
|
7
|
+
import type { AiConfig } from './types'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MODEL = 'gpt-4o'
|
|
10
|
+
const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small'
|
|
11
|
+
|
|
12
|
+
function loadOpenAi(): any {
|
|
13
|
+
try {
|
|
14
|
+
return require('openai').default ?? require('openai')
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('The OpenAI provider requires the openai package. Run: npm install openai')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** OpenAI provider, backed by the official openai SDK. */
|
|
21
|
+
export class OpenAiProvider implements NixxieAiService {
|
|
22
|
+
private config: AiConfig
|
|
23
|
+
private model: string
|
|
24
|
+
private maxTokens: number
|
|
25
|
+
private client: any
|
|
26
|
+
|
|
27
|
+
constructor(config: AiConfig) {
|
|
28
|
+
this.config = config
|
|
29
|
+
this.model = config.model ?? DEFAULT_MODEL
|
|
30
|
+
this.maxTokens = config.maxTokens ?? 4096
|
|
31
|
+
const OpenAI = loadOpenAi()
|
|
32
|
+
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async complete(prompt: string, options?: NixxieAiCompleteOptions): Promise<NixxieAiCompletion> {
|
|
36
|
+
return this.chat([{ role: 'user', content: prompt }], options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async chat(
|
|
40
|
+
messages: NixxieAiMessage[],
|
|
41
|
+
options?: NixxieAiCompleteOptions
|
|
42
|
+
): Promise<NixxieAiCompletion> {
|
|
43
|
+
const allMessages = options?.system
|
|
44
|
+
? [{ role: 'system' as const, content: options.system }, ...messages]
|
|
45
|
+
: messages
|
|
46
|
+
|
|
47
|
+
const model = options?.model ?? this.model
|
|
48
|
+
const response = await this.client.chat.completions.create({
|
|
49
|
+
model,
|
|
50
|
+
max_tokens: options?.maxTokens ?? this.maxTokens,
|
|
51
|
+
messages: allMessages,
|
|
52
|
+
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
text: response.choices?.[0]?.message?.content ?? '',
|
|
57
|
+
model: response.model ?? model,
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: response.usage?.prompt_tokens,
|
|
60
|
+
outputTokens: response.usage?.completion_tokens,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async embed(text: string): Promise<number[]> {
|
|
66
|
+
const [vector] = await this.embedMany([text])
|
|
67
|
+
if (!vector) throw new Error('Embedding provider returned no vector')
|
|
68
|
+
return vector
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async embedMany(texts: string[]): Promise<number[][]> {
|
|
72
|
+
const response = await this.client.embeddings.create({
|
|
73
|
+
model: this.config.embeddingModel ?? DEFAULT_EMBEDDING_MODEL,
|
|
74
|
+
input: texts,
|
|
75
|
+
})
|
|
76
|
+
const items = (response.data ?? []) as Array<{ index?: number; embedding: number[] }>
|
|
77
|
+
if (items.length !== texts.length) {
|
|
78
|
+
throw new Error(`OpenAI returned ${items.length} embeddings for ${texts.length} inputs`)
|
|
79
|
+
}
|
|
80
|
+
// The API does not guarantee response order matches input order — sort by `index`.
|
|
81
|
+
return items
|
|
82
|
+
.slice()
|
|
83
|
+
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
84
|
+
.map(d => d.embedding)
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NixxieAiService } from '@nixxie-cms/core'
|
|
2
|
+
import { AnthropicProvider } from './AnthropicProvider'
|
|
3
|
+
import { OpenAiProvider } from './OpenAiProvider'
|
|
4
|
+
import type { AiConfig } from './types'
|
|
5
|
+
|
|
6
|
+
export function createAi(config: AiConfig): NixxieAiService {
|
|
7
|
+
switch (config.provider ?? 'anthropic') {
|
|
8
|
+
case 'anthropic':
|
|
9
|
+
return new AnthropicProvider(config)
|
|
10
|
+
case 'openai':
|
|
11
|
+
return new OpenAiProvider(config)
|
|
12
|
+
default: {
|
|
13
|
+
const exhaustive: never = config.provider as never
|
|
14
|
+
throw new Error(`Unknown AI provider: ${exhaustive}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { AnthropicProvider, OpenAiProvider }
|
|
20
|
+
export type { AiConfig, AiProvider } from './types'
|
|
21
|
+
export type {
|
|
22
|
+
NixxieAiService,
|
|
23
|
+
NixxieAiMessage,
|
|
24
|
+
NixxieAiCompleteOptions,
|
|
25
|
+
NixxieAiCompletion,
|
|
26
|
+
} from '@nixxie-cms/core'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NixxieAiCompleteOptions,
|
|
3
|
+
NixxieAiCompletion,
|
|
4
|
+
NixxieAiMessage,
|
|
5
|
+
NixxieAiService,
|
|
6
|
+
} from '@nixxie-cms/core'
|
|
7
|
+
|
|
8
|
+
export type { NixxieAiCompleteOptions, NixxieAiCompletion, NixxieAiMessage, NixxieAiService }
|
|
9
|
+
|
|
10
|
+
export type AiProvider = 'anthropic' | 'openai'
|
|
11
|
+
|
|
12
|
+
export type AiConfig = {
|
|
13
|
+
/** LLM provider. Default: 'anthropic'. */
|
|
14
|
+
provider?: AiProvider
|
|
15
|
+
/** API key for the provider. */
|
|
16
|
+
apiKey: string
|
|
17
|
+
/**
|
|
18
|
+
* Default model for completions. Defaults to 'claude-opus-4-8' (Anthropic) or 'gpt-4o' (OpenAI).
|
|
19
|
+
*/
|
|
20
|
+
model?: string
|
|
21
|
+
/** Override the API base URL (proxies, gateways, Azure, etc.). */
|
|
22
|
+
baseUrl?: string
|
|
23
|
+
/** Default max output tokens. Default: 4096. */
|
|
24
|
+
maxTokens?: number
|
|
25
|
+
/**
|
|
26
|
+
* Model used for `embed`/`embedMany`. Anthropic has no native embeddings endpoint, so for the
|
|
27
|
+
* Anthropic provider embeddings are produced via Voyage AI (set `voyageApiKey`). Defaults to
|
|
28
|
+
* 'voyage-3' (Voyage) or 'text-embedding-3-small' (OpenAI).
|
|
29
|
+
*/
|
|
30
|
+
embeddingModel?: string
|
|
31
|
+
/** Voyage AI key, used for embeddings when `provider` is 'anthropic'. */
|
|
32
|
+
voyageApiKey?: string
|
|
33
|
+
}
|