@kognitivedev/vercel-ai-provider 0.2.5 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @kognitivedev/vercel-ai-provider
2
2
 
3
+ ## 0.2.8
4
+
5
+ ### Patch Changes
6
+
7
+ - release
8
+
9
+ - Updated dependencies []:
10
+ - @kognitivedev/prompthub@0.1.6
11
+ - @kognitivedev/shared@0.2.8
12
+
13
+ ## 0.2.7
14
+
15
+ ### Patch Changes
16
+
17
+ - release
18
+
19
+ - Updated dependencies []:
20
+ - @kognitivedev/prompthub@0.1.5
21
+ - @kognitivedev/shared@0.2.7
22
+
23
+ ## 0.2.6
24
+
25
+ ### Patch Changes
26
+
27
+ - release
28
+
29
+ - Updated dependencies []:
30
+ - @kognitivedev/prompthub@0.1.4
31
+ - @kognitivedev/shared@0.2.6
32
+
3
33
  ## 0.2.5
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -109,7 +109,6 @@ type CognitiveLayer = CLModelWrapper & {
109
109
  resolvePrompt: (slug: string) => Promise<CachedPrompt>;
110
110
  logConversation: (payload: LogConversationPayload) => Promise<void>;
111
111
  triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
112
- clearPromptCache: () => void;
113
112
  clearSessionCache: (sessionKey?: string) => void;
114
113
  };
115
114
  ```
@@ -503,16 +502,6 @@ cl.triggerProcessing("user-123", "my-project", "session-abc");
503
502
 
504
503
  ---
505
504
 
506
- ### `cl.clearPromptCache()`
507
-
508
- Clears all cached prompts, forcing the next `resolvePrompt` call to fetch fresh data from the backend.
509
-
510
- ```typescript
511
- cl.clearPromptCache();
512
- ```
513
-
514
- ---
515
-
516
505
  ### `cl.clearSessionCache(sessionKey?)`
517
506
 
518
507
  Clears cached memory snapshots. Pass a specific session key (`"userId:projectId:sessionId"`) to clear one session, or omit to clear all.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const aiMocks = vitest_1.vi.hoisted(() => ({
5
+ wrapLanguageModel: vitest_1.vi.fn(({ model }) => model),
6
+ streamText: vitest_1.vi.fn(async (options) => ({ options })),
7
+ generateText: vitest_1.vi.fn(async (options) => ({ options })),
8
+ }));
9
+ const promptHubMocks = vitest_1.vi.hoisted(() => ({
10
+ resolvePrompt: vitest_1.vi.fn(),
11
+ }));
12
+ vitest_1.vi.mock("ai", () => ({
13
+ wrapLanguageModel: aiMocks.wrapLanguageModel,
14
+ streamText: aiMocks.streamText,
15
+ generateText: aiMocks.generateText,
16
+ }));
17
+ vitest_1.vi.mock("@kognitivedev/prompthub", () => ({
18
+ createPromptHubClient: vitest_1.vi.fn(() => ({
19
+ resolvePrompt: promptHubMocks.resolvePrompt,
20
+ })),
21
+ }));
22
+ const index_1 = require("../index");
23
+ function makeLayer(overrides) {
24
+ return (0, index_1.createCognitiveLayer)({
25
+ provider: (modelId) => ({ modelId }),
26
+ clConfig: Object.assign({ apiKey: "test-api-key", baseUrl: "https://backend.example", projectId: "project-1", processDelayMs: 0, logLevel: "debug" }, overrides),
27
+ });
28
+ }
29
+ (0, vitest_1.describe)("createCognitiveLayer extras", () => {
30
+ (0, vitest_1.beforeEach)(() => {
31
+ aiMocks.wrapLanguageModel.mockClear();
32
+ aiMocks.streamText.mockClear();
33
+ aiMocks.generateText.mockClear();
34
+ promptHubMocks.resolvePrompt.mockReset();
35
+ });
36
+ (0, vitest_1.afterEach)(() => {
37
+ vitest_1.vi.restoreAllMocks();
38
+ });
39
+ (0, vitest_1.it)("resolves prompts and renders variables", async () => {
40
+ promptHubMocks.resolvePrompt.mockResolvedValue({
41
+ promptId: "prompt-1",
42
+ slug: "welcome",
43
+ version: 4,
44
+ content: "Hello {{name}}",
45
+ fetchedAt: Date.now(),
46
+ gatewaySlug: "gateway-a",
47
+ });
48
+ const warnSpy = vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
49
+ const logSpy = vitest_1.vi.spyOn(console, "log").mockImplementation(() => { });
50
+ const cl = makeLayer();
51
+ const model = cl("mock-model", {
52
+ userId: "user-1",
53
+ projectId: "project-1",
54
+ sessionId: "session-1",
55
+ });
56
+ await cl.streamText({
57
+ model,
58
+ messages: [{ role: "user", content: "Hi" }],
59
+ prompt: { slug: "welcome", variables: { name: "Ada" } },
60
+ });
61
+ (0, vitest_1.expect)(promptHubMocks.resolvePrompt).toHaveBeenCalledWith({
62
+ slug: "welcome",
63
+ userId: "user-1",
64
+ tag: undefined,
65
+ });
66
+ (0, vitest_1.expect)(aiMocks.streamText).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
67
+ system: "Hello Ada",
68
+ model: vitest_1.expect.objectContaining({ modelId: "mock-model" }),
69
+ }));
70
+ (0, vitest_1.expect)(warnSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining("Gateway config found but no providerFactory provided"));
71
+ (0, vitest_1.expect)(logSpy).toHaveBeenCalled();
72
+ cl.clearSessionCache("user-1:project-1:session-1");
73
+ cl.clearSessionCache();
74
+ });
75
+ (0, vitest_1.it)("uses a gateway model when providerFactory is available", async () => {
76
+ promptHubMocks.resolvePrompt.mockResolvedValue({
77
+ promptId: "prompt-2",
78
+ slug: "gateway",
79
+ version: 1,
80
+ content: "Prompt body",
81
+ fetchedAt: Date.now(),
82
+ gatewaySlug: "gateway-a",
83
+ });
84
+ const providerFactory = vitest_1.vi.fn(() => vitest_1.vi.fn((modelId) => ({ modelId })));
85
+ const cl = makeLayer({ providerFactory });
86
+ const model = cl("mock-model", {
87
+ userId: "user-1",
88
+ projectId: "project-1",
89
+ sessionId: "session-1",
90
+ });
91
+ await cl.generateText({
92
+ model,
93
+ messages: [{ role: "user", content: "Hi" }],
94
+ prompt: { slug: "gateway" },
95
+ });
96
+ (0, vitest_1.expect)(providerFactory).toHaveBeenCalledWith("https://backend.example/api/cognitive/gateway/gateway-a", "test-api-key");
97
+ (0, vitest_1.expect)(aiMocks.generateText).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
98
+ model: vitest_1.expect.objectContaining({ modelId: "gateway-model" }),
99
+ system: "Prompt body",
100
+ }));
101
+ });
102
+ (0, vitest_1.it)("passes prompt tag and stores tag/ab metadata in logging payload", async () => {
103
+ var _a, _b, _c, _d;
104
+ const backendResponse = {
105
+ promptId: "prompt-4",
106
+ slug: "tagged-welcome",
107
+ version: 2,
108
+ content: "Tagged {{name}}",
109
+ fetchedAt: Date.now(),
110
+ gatewaySlug: null,
111
+ tag: "production",
112
+ abTestId: "ab-test-1",
113
+ variant: "variant",
114
+ };
115
+ promptHubMocks.resolvePrompt.mockResolvedValue(backendResponse);
116
+ const fetchSpy = vitest_1.vi
117
+ .spyOn(globalThis, "fetch")
118
+ .mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 201 }));
119
+ const cl = makeLayer();
120
+ const model = cl("mock-model", {
121
+ userId: "user-2",
122
+ projectId: "project-1",
123
+ sessionId: "session-2",
124
+ });
125
+ await cl.generateText({
126
+ model,
127
+ messages: [{ role: "user", content: "Hi {{name}}" }],
128
+ prompt: { slug: "welcome", tag: "production", variables: { name: "Ada" } },
129
+ });
130
+ await Promise.resolve();
131
+ (0, vitest_1.expect)(promptHubMocks.resolvePrompt).toHaveBeenCalledWith({
132
+ slug: "welcome",
133
+ userId: "user-2",
134
+ tag: "production",
135
+ });
136
+ (0, vitest_1.expect)(fetchSpy).toHaveBeenCalledWith("https://backend.example/api/cognitive/log", vitest_1.expect.objectContaining({
137
+ method: "POST",
138
+ headers: vitest_1.expect.objectContaining({
139
+ "Content-Type": "application/json",
140
+ Authorization: "Bearer test-api-key",
141
+ }),
142
+ body: vitest_1.expect.stringContaining('"promptSlug":"tagged-welcome"'),
143
+ }));
144
+ const logCall = fetchSpy.mock.calls.find(([url]) => String(url).includes("api/cognitive/log"));
145
+ (0, vitest_1.expect)(logCall).toBeDefined();
146
+ const calledWithBody = JSON.parse(logCall[1].body);
147
+ (0, vitest_1.expect)(calledWithBody.promptSlug).toBe("tagged-welcome");
148
+ (0, vitest_1.expect)(calledWithBody.promptVersion).toBe(2);
149
+ (0, vitest_1.expect)(calledWithBody.promptId).toBe("prompt-4");
150
+ (0, vitest_1.expect)(calledWithBody.tag).toBe("production");
151
+ (0, vitest_1.expect)(calledWithBody.abTestId).toBe("ab-test-1");
152
+ (0, vitest_1.expect)(calledWithBody.variant).toBe("variant");
153
+ (0, vitest_1.expect)((_a = calledWithBody.metadata) === null || _a === void 0 ? void 0 : _a.appId).toBeUndefined();
154
+ (0, vitest_1.expect)((_b = calledWithBody.metadata) === null || _b === void 0 ? void 0 : _b.promptTag).toBe("production");
155
+ (0, vitest_1.expect)((_c = calledWithBody.metadata) === null || _c === void 0 ? void 0 : _c.abTestId).toBe("ab-test-1");
156
+ (0, vitest_1.expect)((_d = calledWithBody.metadata) === null || _d === void 0 ? void 0 : _d.variant).toBe("variant");
157
+ fetchSpy.mockRestore();
158
+ });
159
+ (0, vitest_1.it)("falls back to the original model when gateway model creation fails", async () => {
160
+ promptHubMocks.resolvePrompt.mockResolvedValue({
161
+ promptId: "prompt-3",
162
+ slug: "gateway",
163
+ version: 1,
164
+ content: "Prompt body",
165
+ fetchedAt: Date.now(),
166
+ gatewaySlug: "gateway-a",
167
+ });
168
+ const errorSpy = vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
169
+ const cl = makeLayer({
170
+ providerFactory: () => {
171
+ throw new Error("gateway failure");
172
+ },
173
+ });
174
+ const model = cl("mock-model", {
175
+ userId: "user-1",
176
+ projectId: "project-1",
177
+ sessionId: "session-1",
178
+ });
179
+ await cl.streamText({
180
+ model,
181
+ messages: [{ role: "user", content: "Hi" }],
182
+ prompt: { slug: "gateway" },
183
+ });
184
+ (0, vitest_1.expect)(errorSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining("Failed to create gateway model, falling back to original"), vitest_1.expect.any(Error));
185
+ (0, vitest_1.expect)(aiMocks.streamText).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
186
+ model: vitest_1.expect.objectContaining({ modelId: "mock-model" }),
187
+ }));
188
+ });
189
+ (0, vitest_1.it)("falls back cleanly when prompt resolution fails", async () => {
190
+ promptHubMocks.resolvePrompt.mockRejectedValue(new Error("not found"));
191
+ const warnSpy = vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
192
+ const cl = makeLayer();
193
+ const model = cl("mock-model", {
194
+ userId: "user-1",
195
+ projectId: "project-1",
196
+ sessionId: "session-1",
197
+ });
198
+ await cl.generateText({
199
+ model,
200
+ messages: [{ role: "user", content: "Hi" }],
201
+ prompt: { slug: "missing" },
202
+ });
203
+ (0, vitest_1.expect)(warnSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to resolve prompt "missing", generating without system prompt.'), vitest_1.expect.any(Error));
204
+ (0, vitest_1.expect)(aiMocks.generateText).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
205
+ model: vitest_1.expect.objectContaining({ modelId: "mock-model" }),
206
+ }));
207
+ (0, vitest_1.expect)(aiMocks.generateText.mock.calls[0][0]).not.toHaveProperty("system");
208
+ });
209
+ });
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export type CLModelWrapper = (modelId: string, settings?: {
38
38
  export interface PromptConfig {
39
39
  slug: string;
40
40
  variables?: Record<string, string | boolean>;
41
+ tag?: string;
41
42
  }
42
43
  export type CLStreamTextOptions = Omit<Parameters<typeof aiStreamText>[0], 'system' | 'prompt'> & {
43
44
  prompt: PromptConfig;
@@ -56,6 +57,9 @@ export interface LogConversationPayload {
56
57
  promptSlug?: string;
57
58
  promptVersion?: number;
58
59
  promptId?: string;
60
+ tag?: string;
61
+ abTestId?: string;
62
+ variant?: "control" | "variant";
59
63
  traceId?: string;
60
64
  parentSpanId?: string;
61
65
  requestPreview?: string;
@@ -87,10 +91,12 @@ export interface LogConversationPayload {
87
91
  export type CognitiveLayer = CLModelWrapper & {
88
92
  streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
89
93
  generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
90
- resolvePrompt: (slug: string, userId?: string) => Promise<CachedPrompt>;
94
+ resolvePrompt: (slug: string, userId?: string | {
95
+ userId?: string;
96
+ tag?: string;
97
+ }) => Promise<CachedPrompt>;
91
98
  logConversation: (payload: LogConversationPayload) => Promise<void>;
92
99
  triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
93
- clearPromptCache: () => void;
94
100
  clearSessionCache: (sessionKey?: string) => void;
95
101
  };
96
102
  export interface CachedPrompt {
@@ -100,6 +106,9 @@ export interface CachedPrompt {
100
106
  content: string;
101
107
  fetchedAt: number;
102
108
  gatewaySlug?: string;
109
+ tag?: string;
110
+ abTestId?: string;
111
+ variant?: "control" | "variant";
103
112
  }
104
113
  export declare function createCognitiveLayer(config: {
105
114
  provider: any;
package/dist/index.js CHANGED
@@ -214,15 +214,21 @@ function createCognitiveLayer(config) {
214
214
  apiKey: clConfig.apiKey,
215
215
  logger,
216
216
  });
217
- const resolvePrompt = async (slug, userId) => {
217
+ const resolvePrompt = async (slug, userIdOrOptions) => {
218
218
  var _a;
219
+ const userId = typeof userIdOrOptions === "string"
220
+ ? userIdOrOptions
221
+ : userIdOrOptions === null || userIdOrOptions === void 0 ? void 0 : userIdOrOptions.userId;
222
+ const tag = typeof userIdOrOptions === "string"
223
+ ? undefined
224
+ : userIdOrOptions === null || userIdOrOptions === void 0 ? void 0 : userIdOrOptions.tag;
219
225
  logger.debug("Resolving prompt from backend", {
220
226
  slug,
221
227
  userId,
222
228
  baseUrl,
223
229
  apiKeyHint: maskSecret(clConfig.apiKey),
224
230
  });
225
- const data = await promptClient.resolvePrompt({ slug, userId });
231
+ const data = await promptClient.resolvePrompt({ slug, userId, tag });
226
232
  const entry = {
227
233
  promptId: data.promptId,
228
234
  slug: data.slug,
@@ -230,6 +236,9 @@ function createCognitiveLayer(config) {
230
236
  content: data.content,
231
237
  fetchedAt: Date.now(),
232
238
  gatewaySlug: data.gatewaySlug,
239
+ tag: data.tag,
240
+ abTestId: data.abTestId,
241
+ variant: data.variant,
233
242
  };
234
243
  logger.debug("Prompt resolved payload", {
235
244
  slug,
@@ -444,10 +453,11 @@ ${userContextBlock || "None"}
444
453
  promptSlug: promptMeta.promptSlug,
445
454
  promptVersion: promptMeta.promptVersion,
446
455
  promptId: promptMeta.promptId,
456
+ tag: promptMeta.tag,
457
+ abTestId: promptMeta.abTestId,
458
+ variant: promptMeta.variant,
447
459
  })), (toolDefs && { tools: toolDefs })), (agentRunId && { agentRunId })), { traceId: (0, crypto_1.randomUUID)(), requestPreview,
448
- responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
449
- appId: clConfig.appId,
450
- }, spans })).then(() => triggerProcessing(userId, projectId, sessionId));
460
+ responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: Object.assign(Object.assign(Object.assign({ appId: clConfig.appId }, ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.tag) && { promptTag: promptMeta.tag })), ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.abTestId) && { abTestId: promptMeta.abTestId })), ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.variant) && { variant: promptMeta.variant })), spans })).then(() => triggerProcessing(userId, projectId, sessionId));
451
461
  }
452
462
  return result;
453
463
  },
@@ -552,11 +562,12 @@ ${userContextBlock || "None"}
552
562
  promptSlug: promptMeta.promptSlug,
553
563
  promptVersion: promptMeta.promptVersion,
554
564
  promptId: promptMeta.promptId,
565
+ tag: promptMeta.tag,
566
+ abTestId: promptMeta.abTestId,
567
+ variant: promptMeta.variant,
555
568
  })), (toolDefs && { tools: toolDefs })), (agentRunId && { agentRunId })), { traceId,
556
569
  requestPreview,
557
- responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
558
- appId: clConfig.appId,
559
- }, spans })).then(() => triggerProcessing(userId, projectId, sessionId))
570
+ responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: Object.assign(Object.assign(Object.assign({ appId: clConfig.appId }, ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.tag) && { promptTag: promptMeta.tag })), ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.abTestId) && { abTestId: promptMeta.abTestId })), ((promptMeta === null || promptMeta === void 0 ? void 0 : promptMeta.variant) && { variant: promptMeta.variant })), spans })).then(() => triggerProcessing(userId, projectId, sessionId))
560
571
  .catch((e) => logger.error("Stream log failed", e));
561
572
  }
562
573
  });
@@ -621,7 +632,10 @@ ${userContextBlock || "None"}
621
632
  // Resolve and interpolate prompt (graceful fallback on failure)
622
633
  let resolved = null;
623
634
  try {
624
- resolved = await resolvePrompt(promptConfig.slug, session === null || session === void 0 ? void 0 : session.userId);
635
+ resolved = await resolvePrompt(promptConfig.slug, {
636
+ userId: session === null || session === void 0 ? void 0 : session.userId,
637
+ tag: promptConfig.tag,
638
+ });
625
639
  }
626
640
  catch (err) {
627
641
  logger.warn(`Failed to resolve prompt "${promptConfig.slug}", streaming without system prompt.`, err);
@@ -638,6 +652,9 @@ ${userContextBlock || "None"}
638
652
  promptSlug: resolved.slug,
639
653
  promptVersion: resolved.version,
640
654
  promptId: resolved.promptId,
655
+ tag: resolved.tag,
656
+ abTestId: resolved.abTestId,
657
+ variant: resolved.variant,
641
658
  });
642
659
  }
643
660
  logger.info("cl.streamText called", {
@@ -660,7 +677,10 @@ ${userContextBlock || "None"}
660
677
  // Resolve and interpolate prompt (graceful fallback on failure)
661
678
  let resolved = null;
662
679
  try {
663
- resolved = await resolvePrompt(promptConfig.slug, session === null || session === void 0 ? void 0 : session.userId);
680
+ resolved = await resolvePrompt(promptConfig.slug, {
681
+ userId: session === null || session === void 0 ? void 0 : session.userId,
682
+ tag: promptConfig.tag,
683
+ });
664
684
  }
665
685
  catch (err) {
666
686
  logger.warn(`Failed to resolve prompt "${promptConfig.slug}", generating without system prompt.`, err);
@@ -677,6 +697,9 @@ ${userContextBlock || "None"}
677
697
  promptSlug: resolved.slug,
678
698
  promptVersion: resolved.version,
679
699
  promptId: resolved.promptId,
700
+ tag: resolved.tag,
701
+ abTestId: resolved.abTestId,
702
+ variant: resolved.variant,
680
703
  });
681
704
  }
682
705
  logger.info("cl.generateText called", {
@@ -700,7 +723,6 @@ ${userContextBlock || "None"}
700
723
  resolvePrompt,
701
724
  logConversation,
702
725
  triggerProcessing,
703
- clearPromptCache: () => promptClient.clearPromptCache(),
704
726
  clearSessionCache: (sessionKey) => {
705
727
  if (sessionKey) {
706
728
  sessionSnapshots.delete(sessionKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kognitivedev/vercel-ai-provider",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "publishConfig": {
@@ -13,8 +13,8 @@
13
13
  "prepublishOnly": "npm run build"
14
14
  },
15
15
  "dependencies": {
16
- "@kognitivedev/prompthub": "^0.1.3",
17
- "@kognitivedev/shared": "^0.2.5"
16
+ "@kognitivedev/prompthub": "^0.1.6",
17
+ "@kognitivedev/shared": "^0.2.8"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "ai": "^5.0.0 || ^6.0.0"
@@ -0,0 +1,270 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type { LanguageModel } from "ai";
3
+
4
+ const aiMocks = vi.hoisted(() => ({
5
+ wrapLanguageModel: vi.fn(({ model }: { model: unknown }) => model),
6
+ streamText: vi.fn(async (options: Record<string, unknown>) => ({ options })),
7
+ generateText: vi.fn(async (options: Record<string, unknown>) => ({ options })),
8
+ }));
9
+
10
+ const promptHubMocks = vi.hoisted(() => ({
11
+ resolvePrompt: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("ai", () => ({
15
+ wrapLanguageModel: aiMocks.wrapLanguageModel,
16
+ streamText: aiMocks.streamText,
17
+ generateText: aiMocks.generateText,
18
+ }));
19
+
20
+ vi.mock("@kognitivedev/prompthub", () => ({
21
+ createPromptHubClient: vi.fn(() => ({
22
+ resolvePrompt: promptHubMocks.resolvePrompt,
23
+ })),
24
+ }));
25
+
26
+ import { createCognitiveLayer } from "../index";
27
+
28
+ function makeLayer(overrides?: {
29
+ providerFactory?: (baseURL: string, apiKey: string) => (modelId: string) => LanguageModel;
30
+ }) {
31
+ return createCognitiveLayer({
32
+ provider: (modelId: string) => ({ modelId }),
33
+ clConfig: {
34
+ apiKey: "test-api-key",
35
+ baseUrl: "https://backend.example",
36
+ projectId: "project-1",
37
+ processDelayMs: 0,
38
+ logLevel: "debug",
39
+ ...overrides,
40
+ },
41
+ });
42
+ }
43
+
44
+ describe("createCognitiveLayer extras", () => {
45
+ beforeEach(() => {
46
+ aiMocks.wrapLanguageModel.mockClear();
47
+ aiMocks.streamText.mockClear();
48
+ aiMocks.generateText.mockClear();
49
+ promptHubMocks.resolvePrompt.mockReset();
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ it("resolves prompts and renders variables", async () => {
57
+ promptHubMocks.resolvePrompt.mockResolvedValue({
58
+ promptId: "prompt-1",
59
+ slug: "welcome",
60
+ version: 4,
61
+ content: "Hello {{name}}",
62
+ fetchedAt: Date.now(),
63
+ gatewaySlug: "gateway-a",
64
+ });
65
+
66
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
67
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
68
+ const cl = makeLayer();
69
+ const model = cl("mock-model", {
70
+ userId: "user-1",
71
+ projectId: "project-1",
72
+ sessionId: "session-1",
73
+ });
74
+
75
+ await cl.streamText({
76
+ model,
77
+ messages: [{ role: "user", content: "Hi" }],
78
+ prompt: { slug: "welcome", variables: { name: "Ada" } },
79
+ } as any);
80
+
81
+ expect(promptHubMocks.resolvePrompt).toHaveBeenCalledWith({
82
+ slug: "welcome",
83
+ userId: "user-1",
84
+ tag: undefined,
85
+ });
86
+ expect(aiMocks.streamText).toHaveBeenCalledWith(
87
+ expect.objectContaining({
88
+ system: "Hello Ada",
89
+ model: expect.objectContaining({ modelId: "mock-model" }),
90
+ })
91
+ );
92
+ expect(warnSpy).toHaveBeenCalledWith(
93
+ expect.stringContaining("Gateway config found but no providerFactory provided")
94
+ );
95
+ expect(logSpy).toHaveBeenCalled();
96
+ cl.clearSessionCache("user-1:project-1:session-1");
97
+ cl.clearSessionCache();
98
+ });
99
+
100
+ it("uses a gateway model when providerFactory is available", async () => {
101
+ promptHubMocks.resolvePrompt.mockResolvedValue({
102
+ promptId: "prompt-2",
103
+ slug: "gateway",
104
+ version: 1,
105
+ content: "Prompt body",
106
+ fetchedAt: Date.now(),
107
+ gatewaySlug: "gateway-a",
108
+ });
109
+
110
+ const providerFactory = vi.fn(() => vi.fn((modelId: string) => ({ modelId } as unknown as LanguageModel)));
111
+ const cl = makeLayer({ providerFactory });
112
+ const model = cl("mock-model", {
113
+ userId: "user-1",
114
+ projectId: "project-1",
115
+ sessionId: "session-1",
116
+ });
117
+
118
+ await cl.generateText({
119
+ model,
120
+ messages: [{ role: "user", content: "Hi" }],
121
+ prompt: { slug: "gateway" },
122
+ } as any);
123
+
124
+ expect(providerFactory).toHaveBeenCalledWith(
125
+ "https://backend.example/api/cognitive/gateway/gateway-a",
126
+ "test-api-key"
127
+ );
128
+ expect(aiMocks.generateText).toHaveBeenCalledWith(
129
+ expect.objectContaining({
130
+ model: expect.objectContaining({ modelId: "gateway-model" }),
131
+ system: "Prompt body",
132
+ })
133
+ );
134
+ });
135
+
136
+ it("passes prompt tag and stores tag/ab metadata in logging payload", async () => {
137
+ const backendResponse = {
138
+ promptId: "prompt-4",
139
+ slug: "tagged-welcome",
140
+ version: 2,
141
+ content: "Tagged {{name}}",
142
+ fetchedAt: Date.now(),
143
+ gatewaySlug: null,
144
+ tag: "production",
145
+ abTestId: "ab-test-1",
146
+ variant: "variant" as const,
147
+ };
148
+ promptHubMocks.resolvePrompt.mockResolvedValue(backendResponse);
149
+
150
+ const fetchSpy = vi
151
+ .spyOn(globalThis, "fetch")
152
+ .mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 201 }));
153
+
154
+ const cl = makeLayer();
155
+ const model = cl("mock-model", {
156
+ userId: "user-2",
157
+ projectId: "project-1",
158
+ sessionId: "session-2",
159
+ });
160
+
161
+ await cl.generateText({
162
+ model,
163
+ messages: [{ role: "user", content: "Hi {{name}}" }],
164
+ prompt: { slug: "welcome", tag: "production", variables: { name: "Ada" } },
165
+ } as any);
166
+
167
+ await Promise.resolve();
168
+ expect(promptHubMocks.resolvePrompt).toHaveBeenCalledWith({
169
+ slug: "welcome",
170
+ userId: "user-2",
171
+ tag: "production",
172
+ });
173
+ expect(fetchSpy).toHaveBeenCalledWith(
174
+ "https://backend.example/api/cognitive/log",
175
+ expect.objectContaining({
176
+ method: "POST",
177
+ headers: expect.objectContaining({
178
+ "Content-Type": "application/json",
179
+ Authorization: "Bearer test-api-key",
180
+ }),
181
+ body: expect.stringContaining('"promptSlug":"tagged-welcome"'),
182
+ })
183
+ );
184
+
185
+ const logCall = fetchSpy.mock.calls.find(
186
+ ([url]) => String(url).includes("api/cognitive/log"),
187
+ );
188
+ expect(logCall).toBeDefined();
189
+ const calledWithBody = JSON.parse(logCall![1]!.body as string);
190
+ expect(calledWithBody.promptSlug).toBe("tagged-welcome");
191
+ expect(calledWithBody.promptVersion).toBe(2);
192
+ expect(calledWithBody.promptId).toBe("prompt-4");
193
+ expect(calledWithBody.tag).toBe("production");
194
+ expect(calledWithBody.abTestId).toBe("ab-test-1");
195
+ expect(calledWithBody.variant).toBe("variant");
196
+ expect(calledWithBody.metadata?.appId).toBeUndefined();
197
+ expect(calledWithBody.metadata?.promptTag).toBe("production");
198
+ expect(calledWithBody.metadata?.abTestId).toBe("ab-test-1");
199
+ expect(calledWithBody.metadata?.variant).toBe("variant");
200
+
201
+ fetchSpy.mockRestore();
202
+ });
203
+
204
+ it("falls back to the original model when gateway model creation fails", async () => {
205
+ promptHubMocks.resolvePrompt.mockResolvedValue({
206
+ promptId: "prompt-3",
207
+ slug: "gateway",
208
+ version: 1,
209
+ content: "Prompt body",
210
+ fetchedAt: Date.now(),
211
+ gatewaySlug: "gateway-a",
212
+ });
213
+
214
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
215
+ const cl = makeLayer({
216
+ providerFactory: () => {
217
+ throw new Error("gateway failure");
218
+ },
219
+ });
220
+ const model = cl("mock-model", {
221
+ userId: "user-1",
222
+ projectId: "project-1",
223
+ sessionId: "session-1",
224
+ });
225
+
226
+ await cl.streamText({
227
+ model,
228
+ messages: [{ role: "user", content: "Hi" }],
229
+ prompt: { slug: "gateway" },
230
+ } as any);
231
+
232
+ expect(errorSpy).toHaveBeenCalledWith(
233
+ expect.stringContaining("Failed to create gateway model, falling back to original"),
234
+ expect.any(Error)
235
+ );
236
+ expect(aiMocks.streamText).toHaveBeenCalledWith(
237
+ expect.objectContaining({
238
+ model: expect.objectContaining({ modelId: "mock-model" }),
239
+ })
240
+ );
241
+ });
242
+
243
+ it("falls back cleanly when prompt resolution fails", async () => {
244
+ promptHubMocks.resolvePrompt.mockRejectedValue(new Error("not found"));
245
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
246
+ const cl = makeLayer();
247
+ const model = cl("mock-model", {
248
+ userId: "user-1",
249
+ projectId: "project-1",
250
+ sessionId: "session-1",
251
+ });
252
+
253
+ await cl.generateText({
254
+ model,
255
+ messages: [{ role: "user", content: "Hi" }],
256
+ prompt: { slug: "missing" },
257
+ } as any);
258
+
259
+ expect(warnSpy).toHaveBeenCalledWith(
260
+ expect.stringContaining('Failed to resolve prompt "missing", generating without system prompt.'),
261
+ expect.any(Error)
262
+ );
263
+ expect(aiMocks.generateText).toHaveBeenCalledWith(
264
+ expect.objectContaining({
265
+ model: expect.objectContaining({ modelId: "mock-model" }),
266
+ })
267
+ );
268
+ expect(aiMocks.generateText.mock.calls[0][0]).not.toHaveProperty("system");
269
+ });
270
+ });
package/src/index.ts CHANGED
@@ -105,6 +105,7 @@ export type CLModelWrapper = (
105
105
  export interface PromptConfig {
106
106
  slug: string;
107
107
  variables?: Record<string, string | boolean>;
108
+ tag?: string;
108
109
  }
109
110
 
110
111
  export type CLStreamTextOptions = Omit<Parameters<typeof aiStreamText>[0], 'system' | 'prompt'> & {
@@ -126,6 +127,9 @@ export interface LogConversationPayload {
126
127
  promptSlug?: string;
127
128
  promptVersion?: number;
128
129
  promptId?: string;
130
+ tag?: string;
131
+ abTestId?: string;
132
+ variant?: "control" | "variant";
129
133
  traceId?: string;
130
134
  parentSpanId?: string;
131
135
  requestPreview?: string;
@@ -154,10 +158,12 @@ export interface LogConversationPayload {
154
158
  export type CognitiveLayer = CLModelWrapper & {
155
159
  streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
156
160
  generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
157
- resolvePrompt: (slug: string, userId?: string) => Promise<CachedPrompt>;
161
+ resolvePrompt: (
162
+ slug: string,
163
+ userId?: string | { userId?: string; tag?: string }
164
+ ) => Promise<CachedPrompt>;
158
165
  logConversation: (payload: LogConversationPayload) => Promise<void>;
159
166
  triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
160
- clearPromptCache: () => void;
161
167
  clearSessionCache: (sessionKey?: string) => void;
162
168
  };
163
169
 
@@ -170,6 +176,9 @@ export interface CachedPrompt {
170
176
  content: string;
171
177
  fetchedAt: number;
172
178
  gatewaySlug?: string;
179
+ tag?: string;
180
+ abTestId?: string;
181
+ variant?: "control" | "variant";
173
182
  }
174
183
 
175
184
  function getContentText(content: any): string {
@@ -309,7 +318,7 @@ const MEMORY_TAG_REGEX = /<MemoryContext>/i;
309
318
  const SESSION_KEY = Symbol.for("cl:session");
310
319
 
311
320
  // Session key → prompt metadata (populated by cl.streamText/cl.generateText, read by middleware)
312
- const sessionPromptMetadata = new Map<string, { promptSlug: string; promptVersion: number; promptId: string }>();
321
+ const sessionPromptMetadata = new Map<string, { promptSlug: string; promptVersion: number; promptId: string; tag?: string; abTestId?: string; variant?: "control" | "variant" }>();
313
322
 
314
323
  /**
315
324
  * Check if any system message already contains a <MemoryContext> block.
@@ -352,7 +361,13 @@ export function createCognitiveLayer(config: {
352
361
  logger,
353
362
  });
354
363
 
355
- const resolvePrompt = async (slug: string, userId?: string): Promise<CachedPrompt> => {
364
+ const resolvePrompt = async (slug: string, userIdOrOptions?: string | { userId?: string; tag?: string }): Promise<CachedPrompt> => {
365
+ const userId = typeof userIdOrOptions === "string"
366
+ ? userIdOrOptions
367
+ : userIdOrOptions?.userId;
368
+ const tag = typeof userIdOrOptions === "string"
369
+ ? undefined
370
+ : userIdOrOptions?.tag;
356
371
  logger.debug("Resolving prompt from backend", {
357
372
  slug,
358
373
  userId,
@@ -360,7 +375,7 @@ export function createCognitiveLayer(config: {
360
375
  apiKeyHint: maskSecret(clConfig.apiKey),
361
376
  });
362
377
 
363
- const data = await promptClient.resolvePrompt({ slug, userId });
378
+ const data = await promptClient.resolvePrompt({ slug, userId, tag });
364
379
  const entry: CachedPrompt = {
365
380
  promptId: data.promptId,
366
381
  slug: data.slug,
@@ -368,6 +383,9 @@ export function createCognitiveLayer(config: {
368
383
  content: data.content,
369
384
  fetchedAt: Date.now(),
370
385
  gatewaySlug: data.gatewaySlug,
386
+ tag: data.tag,
387
+ abTestId: data.abTestId,
388
+ variant: data.variant,
371
389
  };
372
390
  logger.debug("Prompt resolved payload", {
373
391
  slug,
@@ -611,6 +629,9 @@ ${userContextBlock || "None"}
611
629
  promptSlug: promptMeta.promptSlug,
612
630
  promptVersion: promptMeta.promptVersion,
613
631
  promptId: promptMeta.promptId,
632
+ tag: promptMeta.tag,
633
+ abTestId: promptMeta.abTestId,
634
+ variant: promptMeta.variant,
614
635
  }),
615
636
  ...(toolDefs && { tools: toolDefs }),
616
637
  ...(agentRunId && { agentRunId }),
@@ -623,6 +644,9 @@ ${userContextBlock || "None"}
623
644
  durationMs: endedAt.getTime() - startedAt.getTime(),
624
645
  metadata: {
625
646
  appId: clConfig.appId,
647
+ ...(promptMeta?.tag && { promptTag: promptMeta.tag }),
648
+ ...(promptMeta?.abTestId && { abTestId: promptMeta.abTestId }),
649
+ ...(promptMeta?.variant && { variant: promptMeta.variant }),
626
650
  },
627
651
  spans,
628
652
  }).then(() => triggerProcessing(userId, projectId, sessionId));
@@ -742,6 +766,9 @@ ${userContextBlock || "None"}
742
766
  promptSlug: promptMeta.promptSlug,
743
767
  promptVersion: promptMeta.promptVersion,
744
768
  promptId: promptMeta.promptId,
769
+ tag: promptMeta.tag,
770
+ abTestId: promptMeta.abTestId,
771
+ variant: promptMeta.variant,
745
772
  }),
746
773
  ...(toolDefs && { tools: toolDefs }),
747
774
  ...(agentRunId && { agentRunId }),
@@ -754,6 +781,9 @@ ${userContextBlock || "None"}
754
781
  durationMs: endedAt.getTime() - startedAt.getTime(),
755
782
  metadata: {
756
783
  appId: clConfig.appId,
784
+ ...(promptMeta?.tag && { promptTag: promptMeta.tag }),
785
+ ...(promptMeta?.abTestId && { abTestId: promptMeta.abTestId }),
786
+ ...(promptMeta?.variant && { variant: promptMeta.variant }),
757
787
  },
758
788
  spans,
759
789
  }).then(() => triggerProcessing(userId, projectId, sessionId))
@@ -839,7 +869,10 @@ ${userContextBlock || "None"}
839
869
  // Resolve and interpolate prompt (graceful fallback on failure)
840
870
  let resolved: CachedPrompt | null = null;
841
871
  try {
842
- resolved = await resolvePrompt(promptConfig.slug, session?.userId);
872
+ resolved = await resolvePrompt(promptConfig.slug, {
873
+ userId: session?.userId,
874
+ tag: promptConfig.tag,
875
+ });
843
876
  } catch (err) {
844
877
  logger.warn(`Failed to resolve prompt "${promptConfig.slug}", streaming without system prompt.`, err);
845
878
  }
@@ -857,6 +890,9 @@ ${userContextBlock || "None"}
857
890
  promptSlug: resolved.slug,
858
891
  promptVersion: resolved.version,
859
892
  promptId: resolved.promptId,
893
+ tag: resolved.tag,
894
+ abTestId: resolved.abTestId,
895
+ variant: resolved.variant,
860
896
  });
861
897
  }
862
898
 
@@ -883,7 +919,10 @@ ${userContextBlock || "None"}
883
919
  // Resolve and interpolate prompt (graceful fallback on failure)
884
920
  let resolved: CachedPrompt | null = null;
885
921
  try {
886
- resolved = await resolvePrompt(promptConfig.slug, session?.userId);
922
+ resolved = await resolvePrompt(promptConfig.slug, {
923
+ userId: session?.userId,
924
+ tag: promptConfig.tag,
925
+ });
887
926
  } catch (err) {
888
927
  logger.warn(`Failed to resolve prompt "${promptConfig.slug}", generating without system prompt.`, err);
889
928
  }
@@ -901,6 +940,9 @@ ${userContextBlock || "None"}
901
940
  promptSlug: resolved.slug,
902
941
  promptVersion: resolved.version,
903
942
  promptId: resolved.promptId,
943
+ tag: resolved.tag,
944
+ abTestId: resolved.abTestId,
945
+ variant: resolved.variant,
904
946
  });
905
947
  }
906
948
 
@@ -926,7 +968,6 @@ ${userContextBlock || "None"}
926
968
  resolvePrompt,
927
969
  logConversation,
928
970
  triggerProcessing,
929
- clearPromptCache: () => promptClient.clearPromptCache(),
930
971
  clearSessionCache: (sessionKey?: string) => {
931
972
  if (sessionKey) {
932
973
  sessionSnapshots.delete(sessionKey);