@revealui/ai 0.6.3 → 0.7.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/README.md +5 -4
- package/dist/a2a/card.d.ts +9 -0
- package/dist/a2a/card.d.ts.map +1 -1
- package/dist/a2a/card.js +11 -2
- package/dist/embeddings/index.d.ts +8 -0
- package/dist/embeddings/index.d.ts.map +1 -1
- package/dist/embeddings/index.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/llm/cache-utils.d.ts +11 -25
- package/dist/llm/cache-utils.d.ts.map +1 -1
- package/dist/llm/cache-utils.js +27 -27
- package/dist/llm/client.d.ts +28 -8
- package/dist/llm/client.d.ts.map +1 -1
- package/dist/llm/client.js +92 -13
- package/dist/llm/providers/anthropic.d.ts +30 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.js +45 -0
- package/dist/llm/providers/base.d.ts +80 -27
- package/dist/llm/providers/base.d.ts.map +1 -1
- package/dist/llm/providers/base.js +5 -1
- package/dist/llm/providers/groq.d.ts +2 -1
- package/dist/llm/providers/groq.d.ts.map +1 -1
- package/dist/llm/providers/groq.js +14 -0
- package/dist/llm/providers/inference-snaps.d.ts +2 -1
- package/dist/llm/providers/inference-snaps.d.ts.map +1 -1
- package/dist/llm/providers/inference-snaps.js +15 -0
- package/dist/llm/providers/ollama.d.ts +2 -1
- package/dist/llm/providers/ollama.d.ts.map +1 -1
- package/dist/llm/providers/ollama.js +14 -0
- package/dist/llm/providers/openai-compat.d.ts +18 -1
- package/dist/llm/providers/openai-compat.d.ts.map +1 -1
- package/dist/llm/providers/openai-compat.js +50 -3
- package/dist/llm/providers/openai.d.ts +27 -0
- package/dist/llm/providers/openai.d.ts.map +1 -0
- package/dist/llm/providers/openai.js +41 -0
- package/dist/llm/resolve.d.ts +97 -0
- package/dist/llm/resolve.d.ts.map +1 -0
- package/dist/llm/resolve.js +218 -0
- package/dist/llm/server.d.ts +1 -0
- package/dist/llm/server.d.ts.map +1 -1
- package/dist/llm/server.js +2 -0
- package/dist/llm/token-counter.d.ts +18 -0
- package/dist/llm/token-counter.d.ts.map +1 -1
- package/dist/llm/token-counter.js +16 -13
- package/dist/orchestration/runtime.d.ts +6 -12
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +4 -16
- package/dist/orchestration/streaming-runtime.js +1 -1
- package/dist/skills/types.d.ts +6 -6
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ AI system for RevealUI - memory, LLM, orchestration, and tools.
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **Memory System**: CRDT-based persistent memory (Working, Episodic, Semantic)
|
|
19
|
-
- **LLM Integration**: Provider abstractions for
|
|
19
|
+
- **LLM Integration**: Provider abstractions for Ollama and Canonical Inference Snaps (open-model defaults), plus Groq, Anthropic, OpenAI, and HuggingFace as bring-your-own-key cloud adapters. Every adapter calls the vendor's OpenAI-compatible HTTP endpoint with your own key. No proprietary Anthropic or OpenAI SDK.
|
|
20
20
|
- **Agent Orchestration**: Runtime and execution engine for AI agents
|
|
21
21
|
- **Tool Calling**: Tool registry + standard-MCP-client integration (Stage 5.1a)
|
|
22
22
|
- **Vector Search**: Semantic search with pgvector
|
|
@@ -64,8 +64,9 @@ const provider = new InferenceSnapsProvider({
|
|
|
64
64
|
const client = new LLMClient({ provider })
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
Groq, Anthropic, OpenAI, and HuggingFace remain supported as pluggable,
|
|
68
|
+
bring-your-own-key cloud providers. The local inference path is the
|
|
69
|
+
documented default for self-hosted deployments.
|
|
69
70
|
|
|
70
71
|
## MCP tool integration
|
|
71
72
|
|
|
@@ -313,7 +314,7 @@ const state = await persistence.loadCRDTState(crdtId, 'lww_register')
|
|
|
313
314
|
|
|
314
315
|
### LLM Integration
|
|
315
316
|
|
|
316
|
-
Provider abstractions and unified client for Anthropic,
|
|
317
|
+
Provider abstractions and unified client for Ollama, Canonical Inference Snaps, Groq, Anthropic, OpenAI, and HuggingFace.
|
|
317
318
|
|
|
318
319
|
```typescript
|
|
319
320
|
import { LLMClient, createLLMClientFromEnv } from '@revealui/ai/llm/client'
|
package/dist/a2a/card.d.ts
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentDefinition } from '@revealui/contracts';
|
|
10
10
|
import { type A2AAgentCard } from '@revealui/contracts';
|
|
11
|
+
/**
|
|
12
|
+
* Built-in platform agents defer model selection to the inference provider the
|
|
13
|
+
* account configures on the API Keys page. The dispatch runtime resolves the
|
|
14
|
+
* real model from that provider, so the definition itself names no vendor. An
|
|
15
|
+
* empty value means "use the configured provider's model". Claude and other
|
|
16
|
+
* model ids stay valid where an operator wires a compatible provider; they are
|
|
17
|
+
* no longer the hardcoded default.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PROVIDER_RESOLVED_MODEL = "";
|
|
11
20
|
declare class AgentCardRegistry {
|
|
12
21
|
private readonly defs;
|
|
13
22
|
constructor();
|
package/dist/a2a/card.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../src/a2a/card.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,YAAY,EAAyB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../src/a2a/card.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,YAAY,EAAyB,MAAM,qBAAqB,CAAC;AAM/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAoI1C,cAAM,iBAAiB;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsC;;IAO3D,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI;IAIpC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIpC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,OAAO;IAOzF,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAM9D,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE;IAI1C,QAAQ,IAAI,eAAe,EAAE;IAI7B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAG9B;AAED,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,mBAA0B,CAAC;AAEzD,YAAY,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/a2a/card.js
CHANGED
|
@@ -10,6 +10,15 @@ import { agentDefinitionToCard } from '@revealui/contracts';
|
|
|
10
10
|
// =============================================================================
|
|
11
11
|
// Built-in agent definitions
|
|
12
12
|
// =============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Built-in platform agents defer model selection to the inference provider the
|
|
15
|
+
* account configures on the API Keys page. The dispatch runtime resolves the
|
|
16
|
+
* real model from that provider, so the definition itself names no vendor. An
|
|
17
|
+
* empty value means "use the configured provider's model". Claude and other
|
|
18
|
+
* model ids stay valid where an operator wires a compatible provider; they are
|
|
19
|
+
* no longer the hardcoded default.
|
|
20
|
+
*/
|
|
21
|
+
export const PROVIDER_RESOLVED_MODEL = '';
|
|
13
22
|
/** RevealUI platform meta-agent - "The Creator" */
|
|
14
23
|
const THE_CREATOR_DEF = {
|
|
15
24
|
id: 'revealui-creator',
|
|
@@ -18,7 +27,7 @@ const THE_CREATOR_DEF = {
|
|
|
18
27
|
description: 'The RevealUI platform agent. Scaffolds new AI agents, manages agent lifecycles, ' +
|
|
19
28
|
'orchestrates multi-agent workflows, and acts as the primary interface for AI capabilities ' +
|
|
20
29
|
'on the RevealUI platform.',
|
|
21
|
-
model:
|
|
30
|
+
model: PROVIDER_RESOLVED_MODEL,
|
|
22
31
|
systemPrompt: 'You are The Creator, the meta-agent for RevealUI. You design, configure, and deploy ' +
|
|
23
32
|
'purpose-built AI agents for RevealUI users. You have access to agent scaffolding tools, ' +
|
|
24
33
|
'the RevealUI admin, and the billing system.',
|
|
@@ -74,7 +83,7 @@ const TICKET_AGENT_DEF = {
|
|
|
74
83
|
name: 'Ticket Agent',
|
|
75
84
|
description: 'Handles support tickets, resolves user issues, and escalates when needed. ' +
|
|
76
85
|
'Uses the RevealUI admin to create and update tickets.',
|
|
77
|
-
model:
|
|
86
|
+
model: PROVIDER_RESOLVED_MODEL,
|
|
78
87
|
systemPrompt: 'You are the RevealUI Ticket Agent. You help users resolve issues by creating tickets, ' +
|
|
79
88
|
'searching for solutions, and escalating complex problems to the support team.',
|
|
80
89
|
tools: [
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Note: Groq and Anthropic do not support embeddings - use Ollama for embedding tasks.
|
|
8
8
|
*/
|
|
9
9
|
import z from 'zod/v4';
|
|
10
|
+
import { type LLMClient } from '../llm/client.js';
|
|
10
11
|
declare const EmbeddingSchema: z.ZodObject<{
|
|
11
12
|
model: z.ZodString;
|
|
12
13
|
vector: z.ZodArray<z.ZodNumber>;
|
|
@@ -23,6 +24,13 @@ export interface GenerateEmbeddingOptions {
|
|
|
23
24
|
*/
|
|
24
25
|
model?: string;
|
|
25
26
|
cache?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Pre-resolved LLM client (GAP-360). When supplied — by an authenticated
|
|
29
|
+
* caller that ran `resolveLLMClientForRequest` — embeddings use it (per-user
|
|
30
|
+
* BYOK / hosted resolution). When omitted, embeddings fall back to the
|
|
31
|
+
* env-configured client, preserving self-hosted and internal-caller behavior.
|
|
32
|
+
*/
|
|
33
|
+
client?: Pick<LLMClient, 'embed'>;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Generate an embedding for the given text using the configured LLM provider.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/embeddings/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,CAAC,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/embeddings/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,CAAC,MAAM,QAAQ,CAAC;AACvB,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1E,QAAA,MAAM,eAAe;;;;;iBAgBjB,CAAC;AACL,KAAK,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAEjD,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,SAAS,CAAC,CA6BpB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,SAAS,EAAE,CAAC,CAMtB"}
|
package/dist/embeddings/index.js
CHANGED
|
@@ -41,8 +41,9 @@ export async function generateEmbedding(text, options = {}) {
|
|
|
41
41
|
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
|
42
42
|
throw new Error('Text must be a non-empty string');
|
|
43
43
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
44
|
+
// Prefer the caller-resolved client (BYOK / hosted). Fall back to the
|
|
45
|
+
// env-configured client for self-hosted and internal (no-user) callers.
|
|
46
|
+
const client = options.client ?? createLLMClientFromEnv();
|
|
46
47
|
// Ask client to embed - each provider uses its own default model when model is undefined
|
|
47
48
|
const result = await client.embed(text, model ? { model } : undefined);
|
|
48
49
|
// If provider returned batch, pick first
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export * from './llm/provider-health.js';
|
|
|
54
54
|
export * from './llm/providers/base.js';
|
|
55
55
|
export * from './llm/providers/inference-snaps.js';
|
|
56
56
|
export * from './llm/providers/openai-compat.js';
|
|
57
|
+
export * from './llm/resolve.js';
|
|
57
58
|
export * from './llm/token-counter.js';
|
|
58
59
|
export * from './llm/workspace-provider-config.js';
|
|
59
60
|
export * from './memory/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,cAAc,gBAAgB,CAAC;AAE/B,cAAc,kBAAkB,CAAC;AAIjC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,sBAAsB,CAAC;AAErC,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,mBAAmB,CAAC;AAElC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sCAAsC,CAAC;AACrD,cAAc,iCAAiC,CAAC;AAEhD,cAAc,mBAAmB,CAAC;AAElC,cAAc,sBAAsB,CAAC;AAErC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,cAAc,gBAAgB,CAAC;AAE/B,cAAc,kBAAkB,CAAC;AAIjC,cAAc,uBAAuB,CAAC;AAEtC,cAAc,sBAAsB,CAAC;AAErC,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,mBAAmB,CAAC;AAElC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sCAAsC,CAAC;AACrD,cAAc,iCAAiC,CAAC;AAEhD,cAAc,mBAAmB,CAAC;AAElC,cAAc,sBAAsB,CAAC;AAErC,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export * from './llm/provider-health.js';
|
|
|
61
61
|
export * from './llm/providers/base.js';
|
|
62
62
|
export * from './llm/providers/inference-snaps.js';
|
|
63
63
|
export * from './llm/providers/openai-compat.js';
|
|
64
|
+
export * from './llm/resolve.js';
|
|
64
65
|
export * from './llm/token-counter.js';
|
|
65
66
|
export * from './llm/workspace-provider-config.js';
|
|
66
67
|
// Re-export memory system
|
|
@@ -53,7 +53,7 @@ export declare function estimateCacheSavings(inputTokens: number, cacheHitRate:
|
|
|
53
53
|
*
|
|
54
54
|
* @example
|
|
55
55
|
* ```ts
|
|
56
|
-
* const response = await client.chat(messages, {
|
|
56
|
+
* const response = await client.chat(messages, { cacheHint: true })
|
|
57
57
|
* const stats = formatCacheStats(response.usage)
|
|
58
58
|
* console.log(stats)
|
|
59
59
|
* // "Cache: 45% read (2,500 tokens), 10% created (500 tokens)"
|
|
@@ -83,7 +83,7 @@ export declare function shouldCache(content: string, minTokens?: number): boolea
|
|
|
83
83
|
* ],
|
|
84
84
|
* })
|
|
85
85
|
*
|
|
86
|
-
* const response = await client.chat(conversation, {
|
|
86
|
+
* const response = await client.chat(conversation, { cacheHint: true })
|
|
87
87
|
* ```
|
|
88
88
|
*/
|
|
89
89
|
export declare function createCachedConversation(config: {
|
|
@@ -92,31 +92,17 @@ export declare function createCachedConversation(config: {
|
|
|
92
92
|
messages: Message[];
|
|
93
93
|
}): Message[];
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* @deprecated Use `MODEL_PRICING` from `./token-counter.js`. Retained as a thin derived
|
|
96
|
+
* view (the Anthropic-with-cache subset) so existing importers keep one source of truth.
|
|
97
97
|
*/
|
|
98
98
|
export declare const ANTHROPIC_PRICING: {
|
|
99
|
-
readonly 'claude-3-5-sonnet-20241022':
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
readonly cacheWrite: 3.75;
|
|
103
|
-
readonly cacheRead: 0.3;
|
|
104
|
-
};
|
|
105
|
-
readonly 'claude-3-5-haiku-20241022': {
|
|
106
|
-
readonly input: 1;
|
|
107
|
-
readonly output: 5;
|
|
108
|
-
readonly cacheWrite: 1.25;
|
|
109
|
-
readonly cacheRead: 0.1;
|
|
110
|
-
};
|
|
111
|
-
readonly 'claude-3-opus-20240229': {
|
|
112
|
-
readonly input: 15;
|
|
113
|
-
readonly output: 75;
|
|
114
|
-
readonly cacheWrite: 18.75;
|
|
115
|
-
readonly cacheRead: 1.5;
|
|
116
|
-
};
|
|
99
|
+
readonly 'claude-3-5-sonnet-20241022': import("./token-counter.js").ModelPricing;
|
|
100
|
+
readonly 'claude-3-5-haiku-20241022': import("./token-counter.js").ModelPricing;
|
|
101
|
+
readonly 'claude-3-opus-20240229': import("./token-counter.js").ModelPricing;
|
|
117
102
|
};
|
|
118
103
|
/**
|
|
119
|
-
* Calculate actual cost of a request with caching
|
|
104
|
+
* Calculate actual cost of a request with caching, using the unified MODEL_PRICING table.
|
|
105
|
+
* Unknown models price at zero (matching `estimateCost`).
|
|
120
106
|
*
|
|
121
107
|
* @example
|
|
122
108
|
* ```ts
|
|
@@ -128,12 +114,12 @@ export declare const ANTHROPIC_PRICING: {
|
|
|
128
114
|
* cacheReadTokens: 5000,
|
|
129
115
|
* })
|
|
130
116
|
*
|
|
131
|
-
* console.log(`Request cost: $${cost.toFixed(4)}`)
|
|
117
|
+
* console.log(`Request cost: $${cost.total.toFixed(4)}`)
|
|
132
118
|
* console.log(`Savings vs no cache: $${cost.savings.toFixed(4)}`)
|
|
133
119
|
* ```
|
|
134
120
|
*/
|
|
135
121
|
export declare function calculateCacheCost(usage: {
|
|
136
|
-
model:
|
|
122
|
+
model: string;
|
|
137
123
|
promptTokens: number;
|
|
138
124
|
completionTokens: number;
|
|
139
125
|
cacheCreationTokens?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-utils.d.ts","sourceRoot":"","sources":["../../src/llm/cache-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-utils.d.ts","sourceRoot":"","sources":["../../src/llm/cache-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAKnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,qBAAqB,EAAE,MAAM,GAC5B,MAAM,CAeR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,MAAM,GAAG,IAAI,CAqBhB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAItE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB,GAAG,OAAO,EAAE,CAwBZ;AAWD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;CAIpB,CAAC;AAEX;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA6BxE"}
|
package/dist/llm/cache-utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* console-allowed */
|
|
2
|
+
import { MODEL_PRICING } from './token-counter.js';
|
|
2
3
|
/**
|
|
3
4
|
* Mark a message for caching
|
|
4
5
|
* Use this for system prompts, tool definitions, or large context that repeats
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
export function withCache(message) {
|
|
7
8
|
return {
|
|
8
9
|
...message,
|
|
9
|
-
|
|
10
|
+
cache: true,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
@@ -57,7 +58,7 @@ export function estimateCacheSavings(inputTokens, cacheHitRate, cachedTokenPerce
|
|
|
57
58
|
*
|
|
58
59
|
* @example
|
|
59
60
|
* ```ts
|
|
60
|
-
* const response = await client.chat(messages, {
|
|
61
|
+
* const response = await client.chat(messages, { cacheHint: true })
|
|
61
62
|
* const stats = formatCacheStats(response.usage)
|
|
62
63
|
* console.log(stats)
|
|
63
64
|
* // "Cache: 45% read (2,500 tokens), 10% created (500 tokens)"
|
|
@@ -102,7 +103,7 @@ export function shouldCache(content, minTokens = 1024) {
|
|
|
102
103
|
* ],
|
|
103
104
|
* })
|
|
104
105
|
*
|
|
105
|
-
* const response = await client.chat(conversation, {
|
|
106
|
+
* const response = await client.chat(conversation, { cacheHint: true })
|
|
106
107
|
* ```
|
|
107
108
|
*/
|
|
108
109
|
export function createCachedConversation(config) {
|
|
@@ -118,7 +119,7 @@ export function createCachedConversation(config) {
|
|
|
118
119
|
result.push({
|
|
119
120
|
role: 'system',
|
|
120
121
|
content: doc,
|
|
121
|
-
...(isLast ? {
|
|
122
|
+
...(isLast ? { cache: true } : {}),
|
|
122
123
|
});
|
|
123
124
|
});
|
|
124
125
|
}
|
|
@@ -126,32 +127,26 @@ export function createCachedConversation(config) {
|
|
|
126
127
|
result.push(...config.messages);
|
|
127
128
|
return result;
|
|
128
129
|
}
|
|
130
|
+
/** Look up a model's pricing, asserting presence (used only for known-present keys). */
|
|
131
|
+
function requirePricing(model) {
|
|
132
|
+
const pricing = MODEL_PRICING[model];
|
|
133
|
+
if (!pricing) {
|
|
134
|
+
throw new Error(`No pricing entry for model: ${model}`);
|
|
135
|
+
}
|
|
136
|
+
return pricing;
|
|
137
|
+
}
|
|
129
138
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
139
|
+
* @deprecated Use `MODEL_PRICING` from `./token-counter.js`. Retained as a thin derived
|
|
140
|
+
* view (the Anthropic-with-cache subset) so existing importers keep one source of truth.
|
|
132
141
|
*/
|
|
133
142
|
export const ANTHROPIC_PRICING = {
|
|
134
|
-
'claude-3-5-sonnet-20241022':
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
cacheWrite: 3.75, // 25% markup for cache creation
|
|
138
|
-
cacheRead: 0.3, // 90% discount for cache hits
|
|
139
|
-
},
|
|
140
|
-
'claude-3-5-haiku-20241022': {
|
|
141
|
-
input: 1.0,
|
|
142
|
-
output: 5.0,
|
|
143
|
-
cacheWrite: 1.25,
|
|
144
|
-
cacheRead: 0.1,
|
|
145
|
-
},
|
|
146
|
-
'claude-3-opus-20240229': {
|
|
147
|
-
input: 15.0,
|
|
148
|
-
output: 75.0,
|
|
149
|
-
cacheWrite: 18.75,
|
|
150
|
-
cacheRead: 1.5,
|
|
151
|
-
},
|
|
143
|
+
'claude-3-5-sonnet-20241022': requirePricing('claude-3-5-sonnet-20241022'),
|
|
144
|
+
'claude-3-5-haiku-20241022': requirePricing('claude-3-5-haiku-20241022'),
|
|
145
|
+
'claude-3-opus-20240229': requirePricing('claude-3-opus-20240229'),
|
|
152
146
|
};
|
|
153
147
|
/**
|
|
154
|
-
* Calculate actual cost of a request with caching
|
|
148
|
+
* Calculate actual cost of a request with caching, using the unified MODEL_PRICING table.
|
|
149
|
+
* Unknown models price at zero (matching `estimateCost`).
|
|
155
150
|
*
|
|
156
151
|
* @example
|
|
157
152
|
* ```ts
|
|
@@ -163,12 +158,17 @@ export const ANTHROPIC_PRICING = {
|
|
|
163
158
|
* cacheReadTokens: 5000,
|
|
164
159
|
* })
|
|
165
160
|
*
|
|
166
|
-
* console.log(`Request cost: $${cost.toFixed(4)}`)
|
|
161
|
+
* console.log(`Request cost: $${cost.total.toFixed(4)}`)
|
|
167
162
|
* console.log(`Savings vs no cache: $${cost.savings.toFixed(4)}`)
|
|
168
163
|
* ```
|
|
169
164
|
*/
|
|
170
165
|
export function calculateCacheCost(usage) {
|
|
171
|
-
const pricing =
|
|
166
|
+
const pricing = MODEL_PRICING[usage.model] ?? {
|
|
167
|
+
input: 0,
|
|
168
|
+
output: 0,
|
|
169
|
+
cacheWrite: 0,
|
|
170
|
+
cacheRead: 0,
|
|
171
|
+
};
|
|
172
172
|
const uncachedTokens = usage.promptTokens - (usage.cacheCreationTokens || 0) - (usage.cacheReadTokens || 0);
|
|
173
173
|
const costs = {
|
|
174
174
|
input: (uncachedTokens / 1_000_000) * pricing.input,
|
package/dist/llm/client.d.ts
CHANGED
|
@@ -10,7 +10,15 @@ import type { ProviderHealthMonitor } from './provider-health.js';
|
|
|
10
10
|
import type { Embedding, LLMChatOptions, LLMChunk, LLMEmbedOptions, LLMProvider, LLMResponse, LLMStreamOptions, Message } from './providers/base.js';
|
|
11
11
|
import { type CacheStats, type ResponseCacheOptions } from './response-cache.js';
|
|
12
12
|
import { type SemanticCacheOptions, type SemanticCacheStats } from './semantic-cache.js';
|
|
13
|
-
export type LLMProviderType = '
|
|
13
|
+
export type LLMProviderType = 'anthropic' | 'openai' | 'groq' | 'huggingface' | 'ollama' | 'inference-snaps';
|
|
14
|
+
/**
|
|
15
|
+
* Providers reachable from a hosted (serverless) deployment. Localhost-only
|
|
16
|
+
* providers are false. Consumed by PR-2/PR-3 (resolver + settings UI) to pick
|
|
17
|
+
* which providers a hosted account may configure; no runtime consumer yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare const hostedViable: Record<LLMProviderType, boolean>;
|
|
20
|
+
/** True when the provider can serve a hosted (serverless) deployment. */
|
|
21
|
+
export declare function isHostedViable(provider: LLMProviderType): boolean;
|
|
14
22
|
export interface LLMClientConfig {
|
|
15
23
|
provider: LLMProviderType;
|
|
16
24
|
apiKey: string;
|
|
@@ -129,20 +137,24 @@ export declare class LLMClient {
|
|
|
129
137
|
* Create an LLM client from environment variables.
|
|
130
138
|
*
|
|
131
139
|
* When LLM_PROVIDER is not set, auto-detects the provider by checking env vars
|
|
132
|
-
* in priority order: INFERENCE_SNAPS → GROQ → OLLAMA
|
|
133
|
-
*
|
|
134
|
-
* one-line stderr warning so
|
|
140
|
+
* in priority order: INFERENCE_SNAPS → GROQ → OLLAMA → ANTHROPIC_API_KEY →
|
|
141
|
+
* OPENAI_API_KEY. If none are set, defaults to Inference Snaps at
|
|
142
|
+
* http://localhost:9090/v1 (Ubuntu local) and emits a one-line stderr warning so
|
|
143
|
+
* the implicit localhost default is discoverable in logs.
|
|
135
144
|
*
|
|
136
|
-
* All providers use OpenAI-compatible APIs. No proprietary provider SDKs
|
|
145
|
+
* All providers use OpenAI-compatible APIs. No proprietary provider SDKs
|
|
146
|
+
* (Anthropic + OpenAI ride their OpenAI-compatible endpoints).
|
|
137
147
|
*
|
|
138
148
|
* Canonical Inference Snaps is the reference local provider on Ubuntu — offline,
|
|
139
149
|
* silicon-optimized, no API key required. See `providers/inference-snaps.ts` for
|
|
140
150
|
* install docs (`sudo snap install gemma3`, etc.).
|
|
141
151
|
*
|
|
142
152
|
* Provider defaults:
|
|
143
|
-
* inference-snaps → gemma3
|
|
153
|
+
* inference-snaps → gemma3 (base URL defaults to http://localhost:9090/v1)
|
|
144
154
|
* groq → qwen/qwen3-32b
|
|
145
|
-
* ollama → gemma4:e2b
|
|
155
|
+
* ollama → gemma4:e2b (base URL defaults to http://localhost:11434)
|
|
156
|
+
* anthropic → claude-sonnet-4-6 (base URL defaults to https://api.anthropic.com/v1)
|
|
157
|
+
* openai → gpt-4o (base URL defaults to https://api.openai.com/v1)
|
|
146
158
|
*/
|
|
147
159
|
export declare function createLLMClientFromEnv(): LLMClient;
|
|
148
160
|
/**
|
|
@@ -155,8 +167,16 @@ export declare function createLLMClientFromEnv(): LLMClient;
|
|
|
155
167
|
* Returns `null` if the user has no stored keys (callers should fall back
|
|
156
168
|
* to `createLLMClientFromEnv()` or return a 402/feature-unavailable error).
|
|
157
169
|
*
|
|
170
|
+
* When `opts.hostedViableOnly` is set, a stored key whose provider is not
|
|
171
|
+
* hosted-viable (ollama / inference-snaps are localhost-only) resolves to
|
|
172
|
+
* `null` instead of a client. The resolver passes this on hosted deployments so
|
|
173
|
+
* a localhost-only BYOK key can never yield a localhost client — the exact
|
|
174
|
+
* silent-localhost defect GAP-360 closes (spec §6.5, fail-closed).
|
|
175
|
+
*
|
|
158
176
|
* @param userId - The user's ID from the `users` table
|
|
159
177
|
* @param db - A Drizzle NeonDB client instance
|
|
160
178
|
*/
|
|
161
|
-
export declare function createLLMClientForUser(userId: string, db: Database, auditStore?: AuditStore
|
|
179
|
+
export declare function createLLMClientForUser(userId: string, db: Database, auditStore?: Pick<AuditStore, 'append'>, opts?: {
|
|
180
|
+
hostedViableOnly?: boolean;
|
|
181
|
+
}): Promise<LLMClient | null>;
|
|
162
182
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/llm/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/llm/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/llm/client.ts"],"names":[],"mappings":"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"}
|
package/dist/llm/client.js
CHANGED
|
@@ -3,16 +3,42 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single interface for all LLM providers with fallback and rate limiting
|
|
5
5
|
*/
|
|
6
|
+
// Log redaction lives in @revealui/security — import `redactLogContext`
|
|
7
|
+
// (recursive walker) or `redactLogField` (single key/value).
|
|
8
|
+
import { createLogger } from '@revealui/core/observability/logger';
|
|
6
9
|
import { decryptApiKey } from '@revealui/db/crypto';
|
|
7
10
|
import { tenantProviderConfigs, userApiKeys } from '@revealui/db/schema';
|
|
8
11
|
import { CircuitBreaker, CircuitBreakerOpenError, } from '@revealui/resilience';
|
|
9
12
|
import { and, eq } from 'drizzle-orm';
|
|
13
|
+
import { AnthropicProvider } from './providers/anthropic.js';
|
|
10
14
|
import { GroqProvider } from './providers/groq.js';
|
|
11
15
|
import { InferenceSnapsProvider, } from './providers/inference-snaps.js';
|
|
12
16
|
import { OllamaProvider } from './providers/ollama.js';
|
|
17
|
+
import { OpenAIProvider } from './providers/openai.js';
|
|
18
|
+
import { OpenAICompatProvider } from './providers/openai-compat.js';
|
|
13
19
|
import { ResponseCache } from './response-cache.js';
|
|
14
20
|
import { SemanticCache, } from './semantic-cache.js';
|
|
15
21
|
import { estimateRequest as _estimateRequestTokens } from './token-counter.js';
|
|
22
|
+
/**
|
|
23
|
+
* Providers reachable from a hosted (serverless) deployment. Localhost-only
|
|
24
|
+
* providers are false. Consumed by PR-2/PR-3 (resolver + settings UI) to pick
|
|
25
|
+
* which providers a hosted account may configure; no runtime consumer yet.
|
|
26
|
+
*/
|
|
27
|
+
export const hostedViable = {
|
|
28
|
+
anthropic: true,
|
|
29
|
+
openai: true,
|
|
30
|
+
groq: true,
|
|
31
|
+
huggingface: true,
|
|
32
|
+
ollama: false,
|
|
33
|
+
'inference-snaps': false,
|
|
34
|
+
};
|
|
35
|
+
/** True when the provider can serve a hosted (serverless) deployment. */
|
|
36
|
+
export function isHostedViable(provider) {
|
|
37
|
+
return hostedViable[provider];
|
|
38
|
+
}
|
|
39
|
+
/** Emitted once per process when the zero-config localhost default is selected. */
|
|
40
|
+
let warnedLocalhostDefault = false;
|
|
41
|
+
const envFactoryLogger = createLogger({ component: 'createLLMClientFromEnv' });
|
|
16
42
|
export class LLMClient {
|
|
17
43
|
provider;
|
|
18
44
|
fallbackProvider;
|
|
@@ -78,12 +104,22 @@ export class LLMClient {
|
|
|
78
104
|
}
|
|
79
105
|
createProvider(type, config) {
|
|
80
106
|
switch (type) {
|
|
107
|
+
case 'anthropic':
|
|
108
|
+
return new AnthropicProvider(config);
|
|
109
|
+
case 'openai':
|
|
110
|
+
return new OpenAIProvider(config);
|
|
81
111
|
case 'groq':
|
|
82
112
|
return new GroqProvider(config);
|
|
83
113
|
case 'ollama':
|
|
84
114
|
return new OllamaProvider(config);
|
|
85
115
|
case 'inference-snaps':
|
|
86
116
|
return new InferenceSnapsProvider(config);
|
|
117
|
+
case 'huggingface':
|
|
118
|
+
// HuggingFace exposes an OpenAI-compatible inference endpoint; baseURL is
|
|
119
|
+
// per-model (HF_MODEL_URL), so it has no dedicated wrapper — the compat
|
|
120
|
+
// base serves it directly. Fixes the latent defect where the env factory
|
|
121
|
+
// accepted 'huggingface' but createProvider threw 'Unknown provider type'.
|
|
122
|
+
return new OpenAICompatProvider(config);
|
|
87
123
|
default:
|
|
88
124
|
throw new Error(`Unknown provider type: ${String(type)}`);
|
|
89
125
|
}
|
|
@@ -412,23 +448,29 @@ export class LLMClient {
|
|
|
412
448
|
* Create an LLM client from environment variables.
|
|
413
449
|
*
|
|
414
450
|
* When LLM_PROVIDER is not set, auto-detects the provider by checking env vars
|
|
415
|
-
* in priority order: INFERENCE_SNAPS → GROQ → OLLAMA
|
|
416
|
-
*
|
|
417
|
-
* one-line stderr warning so
|
|
451
|
+
* in priority order: INFERENCE_SNAPS → GROQ → OLLAMA → ANTHROPIC_API_KEY →
|
|
452
|
+
* OPENAI_API_KEY. If none are set, defaults to Inference Snaps at
|
|
453
|
+
* http://localhost:9090/v1 (Ubuntu local) and emits a one-line stderr warning so
|
|
454
|
+
* the implicit localhost default is discoverable in logs.
|
|
418
455
|
*
|
|
419
|
-
* All providers use OpenAI-compatible APIs. No proprietary provider SDKs
|
|
456
|
+
* All providers use OpenAI-compatible APIs. No proprietary provider SDKs
|
|
457
|
+
* (Anthropic + OpenAI ride their OpenAI-compatible endpoints).
|
|
420
458
|
*
|
|
421
459
|
* Canonical Inference Snaps is the reference local provider on Ubuntu — offline,
|
|
422
460
|
* silicon-optimized, no API key required. See `providers/inference-snaps.ts` for
|
|
423
461
|
* install docs (`sudo snap install gemma3`, etc.).
|
|
424
462
|
*
|
|
425
463
|
* Provider defaults:
|
|
426
|
-
* inference-snaps → gemma3
|
|
464
|
+
* inference-snaps → gemma3 (base URL defaults to http://localhost:9090/v1)
|
|
427
465
|
* groq → qwen/qwen3-32b
|
|
428
|
-
* ollama → gemma4:e2b
|
|
466
|
+
* ollama → gemma4:e2b (base URL defaults to http://localhost:11434)
|
|
467
|
+
* anthropic → claude-sonnet-4-6 (base URL defaults to https://api.anthropic.com/v1)
|
|
468
|
+
* openai → gpt-4o (base URL defaults to https://api.openai.com/v1)
|
|
429
469
|
*/
|
|
430
470
|
export function createLLMClientFromEnv() {
|
|
431
|
-
// Auto-detect provider when LLM_PROVIDER is not explicitly set
|
|
471
|
+
// Auto-detect provider when LLM_PROVIDER is not explicitly set. The existing
|
|
472
|
+
// priority order (INFERENCE_SNAPS → GROQ → OLLAMA) is preserved so existing
|
|
473
|
+
// deployments resolve identically; the frontier providers are appended after.
|
|
432
474
|
let provider;
|
|
433
475
|
if (process.env.LLM_PROVIDER) {
|
|
434
476
|
provider = process.env.LLM_PROVIDER;
|
|
@@ -442,15 +484,38 @@ export function createLLMClientFromEnv() {
|
|
|
442
484
|
else if (process.env.OLLAMA_BASE_URL) {
|
|
443
485
|
provider = 'ollama';
|
|
444
486
|
}
|
|
487
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
488
|
+
provider = 'anthropic';
|
|
489
|
+
}
|
|
490
|
+
else if (process.env.OPENAI_API_KEY) {
|
|
491
|
+
provider = 'openai';
|
|
492
|
+
}
|
|
445
493
|
else {
|
|
446
|
-
// Zero-config Ubuntu default: assume Inference Snaps on the standard
|
|
447
|
-
//
|
|
494
|
+
// Zero-config Ubuntu default: assume Inference Snaps on the standard local
|
|
495
|
+
// port. This localhost default is unreachable inside a hosted serverless
|
|
496
|
+
// function, so warn once per process to make the implicit choice visible.
|
|
448
497
|
provider = 'inference-snaps';
|
|
498
|
+
if (!warnedLocalhostDefault) {
|
|
499
|
+
warnedLocalhostDefault = true;
|
|
500
|
+
envFactoryLogger.warn('No LLM provider env var set — defaulting to inference-snaps at ' +
|
|
501
|
+
'http://localhost:9090/v1. This localhost endpoint is unreachable on a ' +
|
|
502
|
+
'hosted deployment; set LLM_PROVIDER or a provider key.');
|
|
503
|
+
}
|
|
449
504
|
}
|
|
450
505
|
let apiKey;
|
|
451
506
|
let baseURL;
|
|
452
507
|
let defaultModel;
|
|
453
|
-
if (provider === '
|
|
508
|
+
if (provider === 'anthropic') {
|
|
509
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
510
|
+
baseURL = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com/v1';
|
|
511
|
+
defaultModel = 'claude-sonnet-4-6';
|
|
512
|
+
}
|
|
513
|
+
else if (provider === 'openai') {
|
|
514
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
515
|
+
baseURL = process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1';
|
|
516
|
+
defaultModel = 'gpt-4o';
|
|
517
|
+
}
|
|
518
|
+
else if (provider === 'huggingface') {
|
|
454
519
|
apiKey = process.env.HF_TOKEN;
|
|
455
520
|
baseURL = process.env.HF_MODEL_URL;
|
|
456
521
|
}
|
|
@@ -475,7 +540,8 @@ export function createLLMClientFromEnv() {
|
|
|
475
540
|
}
|
|
476
541
|
if (!apiKey) {
|
|
477
542
|
throw new Error(`API key not found for provider "${provider}". Set the corresponding env var ` +
|
|
478
|
-
`(INFERENCE_SNAPS_BASE_URL, GROQ_API_KEY, OLLAMA_BASE_URL,
|
|
543
|
+
`(INFERENCE_SNAPS_BASE_URL, GROQ_API_KEY, OLLAMA_BASE_URL, HF_TOKEN, ` +
|
|
544
|
+
`ANTHROPIC_API_KEY, or OPENAI_API_KEY).`);
|
|
479
545
|
}
|
|
480
546
|
return new LLMClient({
|
|
481
547
|
provider,
|
|
@@ -501,10 +567,19 @@ export function createLLMClientFromEnv() {
|
|
|
501
567
|
* Returns `null` if the user has no stored keys (callers should fall back
|
|
502
568
|
* to `createLLMClientFromEnv()` or return a 402/feature-unavailable error).
|
|
503
569
|
*
|
|
570
|
+
* When `opts.hostedViableOnly` is set, a stored key whose provider is not
|
|
571
|
+
* hosted-viable (ollama / inference-snaps are localhost-only) resolves to
|
|
572
|
+
* `null` instead of a client. The resolver passes this on hosted deployments so
|
|
573
|
+
* a localhost-only BYOK key can never yield a localhost client — the exact
|
|
574
|
+
* silent-localhost defect GAP-360 closes (spec §6.5, fail-closed).
|
|
575
|
+
*
|
|
504
576
|
* @param userId - The user's ID from the `users` table
|
|
505
577
|
* @param db - A Drizzle NeonDB client instance
|
|
506
578
|
*/
|
|
507
|
-
export async function createLLMClientForUser(userId, db,
|
|
579
|
+
export async function createLLMClientForUser(userId, db,
|
|
580
|
+
// Only `append` is used; the narrow type lets a persistent store whose entry
|
|
581
|
+
// type widens eventType/severity to `string` (e.g. DrizzleAuditStore) fit.
|
|
582
|
+
auditStore, opts) {
|
|
508
583
|
// Find the user's preferred provider config
|
|
509
584
|
const [preferredConfig] = await db
|
|
510
585
|
.select()
|
|
@@ -522,8 +597,12 @@ export async function createLLMClientForUser(userId, db, auditStore) {
|
|
|
522
597
|
const [keyRow] = await keyQuery;
|
|
523
598
|
if (!keyRow)
|
|
524
599
|
return null;
|
|
525
|
-
const plaintext = decryptApiKey(keyRow.encryptedKey);
|
|
526
600
|
const provider = keyRow.provider;
|
|
601
|
+
// Fail-closed hosted filter (§6.5): reject a localhost-only provider before
|
|
602
|
+
// decrypting, so no plaintext is touched for a key we will not use.
|
|
603
|
+
if (opts?.hostedViableOnly && !isHostedViable(provider))
|
|
604
|
+
return null;
|
|
605
|
+
const plaintext = decryptApiKey(keyRow.encryptedKey);
|
|
527
606
|
const model = preferredConfig?.model ?? undefined;
|
|
528
607
|
// Fire-and-forget: record when this key was last used (best-effort, never blocks)
|
|
529
608
|
db.update(userApiKeys)
|