@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.
Files changed (52) hide show
  1. package/README.md +5 -4
  2. package/dist/a2a/card.d.ts +9 -0
  3. package/dist/a2a/card.d.ts.map +1 -1
  4. package/dist/a2a/card.js +11 -2
  5. package/dist/embeddings/index.d.ts +8 -0
  6. package/dist/embeddings/index.d.ts.map +1 -1
  7. package/dist/embeddings/index.js +3 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/llm/cache-utils.d.ts +11 -25
  12. package/dist/llm/cache-utils.d.ts.map +1 -1
  13. package/dist/llm/cache-utils.js +27 -27
  14. package/dist/llm/client.d.ts +28 -8
  15. package/dist/llm/client.d.ts.map +1 -1
  16. package/dist/llm/client.js +92 -13
  17. package/dist/llm/providers/anthropic.d.ts +30 -0
  18. package/dist/llm/providers/anthropic.d.ts.map +1 -0
  19. package/dist/llm/providers/anthropic.js +45 -0
  20. package/dist/llm/providers/base.d.ts +80 -27
  21. package/dist/llm/providers/base.d.ts.map +1 -1
  22. package/dist/llm/providers/base.js +5 -1
  23. package/dist/llm/providers/groq.d.ts +2 -1
  24. package/dist/llm/providers/groq.d.ts.map +1 -1
  25. package/dist/llm/providers/groq.js +14 -0
  26. package/dist/llm/providers/inference-snaps.d.ts +2 -1
  27. package/dist/llm/providers/inference-snaps.d.ts.map +1 -1
  28. package/dist/llm/providers/inference-snaps.js +15 -0
  29. package/dist/llm/providers/ollama.d.ts +2 -1
  30. package/dist/llm/providers/ollama.d.ts.map +1 -1
  31. package/dist/llm/providers/ollama.js +14 -0
  32. package/dist/llm/providers/openai-compat.d.ts +18 -1
  33. package/dist/llm/providers/openai-compat.d.ts.map +1 -1
  34. package/dist/llm/providers/openai-compat.js +50 -3
  35. package/dist/llm/providers/openai.d.ts +27 -0
  36. package/dist/llm/providers/openai.d.ts.map +1 -0
  37. package/dist/llm/providers/openai.js +41 -0
  38. package/dist/llm/resolve.d.ts +97 -0
  39. package/dist/llm/resolve.d.ts.map +1 -0
  40. package/dist/llm/resolve.js +218 -0
  41. package/dist/llm/server.d.ts +1 -0
  42. package/dist/llm/server.d.ts.map +1 -1
  43. package/dist/llm/server.js +2 -0
  44. package/dist/llm/token-counter.d.ts +18 -0
  45. package/dist/llm/token-counter.d.ts.map +1 -1
  46. package/dist/llm/token-counter.js +16 -13
  47. package/dist/orchestration/runtime.d.ts +6 -12
  48. package/dist/orchestration/runtime.d.ts.map +1 -1
  49. package/dist/orchestration/runtime.js +4 -16
  50. package/dist/orchestration/streaming-runtime.js +1 -1
  51. package/dist/skills/types.d.ts +6 -6
  52. 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 Anthropic, GROQ, Ollama, Canonical Inference Snaps, and more
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
- Cloud providers (Anthropic, OpenAI, GROQ) remain supported; the local
68
- inference path is the documented default for self-hosted deployments.
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, GROQ, and Ollama.
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'
@@ -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();
@@ -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;AAwI/E,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"}
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: 'claude-opus-4-6',
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: 'claude-sonnet-4-6',
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;AAGvB,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;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,SAAS,CAAC,CA4BpB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,SAAS,EAAE,CAAC,CAMtB"}
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"}
@@ -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
- // Use unified LLM client - auto-detects provider from env vars
45
- const client = createLLMClientFromEnv();
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';
@@ -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, { enableCache: true })
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, { enableCache: true })
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
- * Pricing information for Anthropic Claude models (as of 2024)
96
- * Prices are per million tokens
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
- readonly input: 3;
101
- readonly output: 15;
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: keyof typeof ANTHROPIC_PRICING;
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;AAEnD;;;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;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;CAmBpB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,KAAK,EAAE,MAAM,OAAO,iBAAiB,CAAC;IACtC,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,CAwBxE"}
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"}
@@ -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
- cacheControl: { type: 'ephemeral' },
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, { enableCache: true })
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, { enableCache: true })
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 ? { cacheControl: { type: 'ephemeral' } } : {}),
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
- * Pricing information for Anthropic Claude models (as of 2024)
131
- * Prices are per million tokens
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
- input: 3.0,
136
- output: 15.0,
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 = ANTHROPIC_PRICING[usage.model];
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,
@@ -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 = 'groq' | 'ollama' | 'huggingface' | 'inference-snaps';
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. If none are set, defaults
133
- * to Inference Snaps at http://localhost:9090/v1 (Ubuntu local) and emits a
134
- * one-line stderr warning so the implicit default is discoverable in logs.
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 (base URL defaults to http://localhost:9090/v1)
153
+ * inference-snaps → gemma3 (base URL defaults to http://localhost:9090/v1)
144
154
  * groq → qwen/qwen3-32b
145
- * ollama → gemma4:e2b (base URL defaults to http://localhost:11434)
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): Promise<LLMClient | null>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/llm/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/llm/client.ts"],"names":[],"mappings":"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"}
@@ -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. If none are set, defaults
416
- * to Inference Snaps at http://localhost:9090/v1 (Ubuntu local) and emits a
417
- * one-line stderr warning so the implicit default is discoverable in logs.
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 (base URL defaults to http://localhost:9090/v1)
464
+ * inference-snaps → gemma3 (base URL defaults to http://localhost:9090/v1)
427
465
  * groq → qwen/qwen3-32b
428
- * ollama → gemma4:e2b (base URL defaults to http://localhost:11434)
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
- // local port. Operator sees the warning once at boot.
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 === 'huggingface') {
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, or HF_TOKEN).`);
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, auditStore) {
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)