@agentguard-run/spend 0.15.10 → 0.15.12

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 (44) hide show
  1. package/README.md +65 -0
  2. package/dist/cli/main.d.ts.map +1 -1
  3. package/dist/cli/main.js +5 -0
  4. package/dist/cli/main.js.map +1 -1
  5. package/dist/cli/route-demo.d.ts +16 -0
  6. package/dist/cli/route-demo.d.ts.map +1 -0
  7. package/dist/cli/route-demo.js +211 -0
  8. package/dist/cli/route-demo.js.map +1 -0
  9. package/dist/frameworks/index.d.ts +2 -0
  10. package/dist/frameworks/index.d.ts.map +1 -1
  11. package/dist/frameworks/index.js +2 -0
  12. package/dist/frameworks/index.js.map +1 -1
  13. package/dist/frameworks/langchain.d.ts +52 -0
  14. package/dist/frameworks/langchain.d.ts.map +1 -0
  15. package/dist/frameworks/langchain.js +84 -0
  16. package/dist/frameworks/langchain.js.map +1 -0
  17. package/dist/frameworks/openai.d.ts +33 -0
  18. package/dist/frameworks/openai.d.ts.map +1 -0
  19. package/dist/frameworks/openai.js +38 -0
  20. package/dist/frameworks/openai.js.map +1 -0
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/router-engine.d.ts +123 -0
  26. package/dist/router-engine.d.ts.map +1 -0
  27. package/dist/router-engine.js +246 -0
  28. package/dist/router-engine.js.map +1 -0
  29. package/dist/router.d.ts +1 -0
  30. package/dist/router.d.ts.map +1 -1
  31. package/dist/router.js +6 -0
  32. package/dist/router.js.map +1 -1
  33. package/dist/spend-guard.d.ts +20 -1
  34. package/dist/spend-guard.d.ts.map +1 -1
  35. package/dist/spend-guard.js +37 -0
  36. package/dist/spend-guard.js.map +1 -1
  37. package/dist/types.d.ts +3 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +11 -1
  40. package/src/frameworks/README.md +35 -0
  41. package/src/frameworks/index.ts +2 -0
  42. package/src/frameworks/langchain.ts +159 -0
  43. package/src/frameworks/openai.ts +36 -0
  44. package/src/router.ts +16 -0
@@ -0,0 +1,159 @@
1
+ import type { SpendGuard } from '../spend-guard';
2
+ import {
3
+ createFrameworkGuard,
4
+ modelFromUnknown,
5
+ objectRecord,
6
+ outputTokensFromParams,
7
+ preflightFrameworkCall,
8
+ recordFrameworkReceipt,
9
+ redactDataPlane,
10
+ settleFrameworkCall,
11
+ stringValue,
12
+ type FrameworkAdapterOptions,
13
+ type FrameworkPreflight,
14
+ } from './common';
15
+
16
+ /**
17
+ * LangChain.js callback handler that runs `guard.decide()` before every LLM /
18
+ * chat-model call and settles real token usage after it completes.
19
+ *
20
+ * Hook point: LangChain's `BaseCallbackHandler` lifecycle methods
21
+ * `handleChatModelStart` / `handleLLMStart` (preflight, may throw
22
+ * AgentGuardBlockedError) and `handleLLMEnd` (usage settlement). We do NOT
23
+ * extend the real BaseCallbackHandler (that would force a peer-dep import at
24
+ * module load); LangChain duck-types handlers by these method names, and we set
25
+ * `awaitHandlers = true` so a thrown block aborts the call.
26
+ *
27
+ * This adapter reuses the shared framework core (createFrameworkGuard +
28
+ * preflightFrameworkCall + settleFrameworkCall). It never proxies prompts,
29
+ * completions, or provider keys — only redacted request shape leaves for the
30
+ * signed receipt.
31
+ *
32
+ * @example
33
+ * import { ChatOpenAI } from '@langchain/openai';
34
+ * import { createLangChainHandler } from '@agentguard-run/spend/frameworks/langchain';
35
+ *
36
+ * const handler = createLangChainHandler({ policy, scope });
37
+ * const llm = new ChatOpenAI({ callbacks: [handler] });
38
+ * await llm.invoke('hello'); // throws AgentGuardBlockedError if a cap blocks
39
+ */
40
+ export interface LangChainAdapterOptions extends FrameworkAdapterOptions {
41
+ framework?: 'langchain';
42
+ }
43
+
44
+ interface LangChainSerialized {
45
+ id?: string[];
46
+ kwargs?: Record<string, unknown>;
47
+ [key: string]: unknown;
48
+ }
49
+
50
+ export class AgentGuardLangChainHandler {
51
+ readonly name = 'agentguard_spend';
52
+ /** Ensures LangChain awaits the handler so a thrown block actually aborts. */
53
+ readonly awaitHandlers = true;
54
+ readonly guard: SpendGuard;
55
+ private readonly opts: LangChainAdapterOptions;
56
+ private readonly pending = new Map<string, FrameworkPreflight>();
57
+
58
+ constructor(opts: LangChainAdapterOptions) {
59
+ this.opts = { ...opts, framework: 'langchain' };
60
+ this.guard = createFrameworkGuard(this.opts);
61
+ }
62
+
63
+ async handleChatModelStart(
64
+ llm: LangChainSerialized,
65
+ messages: unknown[][],
66
+ runId: string,
67
+ _parentRunId?: string,
68
+ extraParams?: Record<string, unknown>,
69
+ _tags?: string[],
70
+ metadata?: Record<string, unknown>,
71
+ ): Promise<void> {
72
+ await this.start(runId, llm, messages, extraParams, metadata);
73
+ }
74
+
75
+ async handleLLMStart(
76
+ llm: LangChainSerialized,
77
+ prompts: unknown[],
78
+ runId: string,
79
+ _parentRunId?: string,
80
+ extraParams?: Record<string, unknown>,
81
+ _tags?: string[],
82
+ metadata?: Record<string, unknown>,
83
+ ): Promise<void> {
84
+ await this.start(runId, llm, prompts, extraParams, metadata);
85
+ }
86
+
87
+ async handleLLMEnd(output: unknown, runId: string): Promise<void> {
88
+ const preflight = this.pending.get(runId);
89
+ if (!preflight) return;
90
+ this.pending.delete(runId);
91
+ await settleFrameworkCall(preflight, normalizeUsage(output));
92
+ await recordFrameworkReceipt(this.guard, 'langchain', {
93
+ event: 'llm_end',
94
+ decisionId: preflight.decision.decisionId,
95
+ model: preflight.decision.modelResolved,
96
+ });
97
+ }
98
+
99
+ async handleLLMError(_error: unknown, runId: string): Promise<void> {
100
+ this.pending.delete(runId);
101
+ }
102
+
103
+ private async start(
104
+ runId: string,
105
+ llm: LangChainSerialized,
106
+ payload: unknown,
107
+ extraParams?: Record<string, unknown>,
108
+ metadata?: Record<string, unknown>,
109
+ ): Promise<void> {
110
+ const invocation = objectRecord(extraParams?.invocation_params) ?? {};
111
+ const model = modelFromUnknown(
112
+ invocation.model ?? invocation.model_name ?? lastId(llm) ?? objectRecord(llm?.kwargs)?.model,
113
+ this.opts.defaultModel ?? 'unknown',
114
+ );
115
+ const preflight = await preflightFrameworkCall(this.guard, this.opts, {
116
+ framework: 'langchain',
117
+ model,
118
+ params: payload,
119
+ outputTokens: outputTokensFromParams(invocation, this.opts.defaultOutputTokens ?? 1024),
120
+ metadata: redactDataPlane(metadata ?? {}) as Record<string, unknown>,
121
+ workflowId: stringValue(metadata?.workflowId) ?? stringValue(metadata?.thread_id),
122
+ requestShape: {
123
+ toolCount: Array.isArray(invocation.tools) ? invocation.tools.length : 0,
124
+ maxTokens: invocation.max_tokens ?? invocation.maxTokens,
125
+ stream: invocation.stream === true,
126
+ },
127
+ });
128
+ this.pending.set(runId, preflight);
129
+ }
130
+ }
131
+
132
+ export function createLangChainHandler(opts: LangChainAdapterOptions): AgentGuardLangChainHandler {
133
+ return new AgentGuardLangChainHandler(opts);
134
+ }
135
+
136
+ export const agentguardLangChainHandler = createLangChainHandler;
137
+
138
+ function lastId(llm: LangChainSerialized | undefined): string | undefined {
139
+ const id = llm?.id;
140
+ return Array.isArray(id) && id.length ? id[id.length - 1] : undefined;
141
+ }
142
+
143
+ /**
144
+ * Map a LangChain LLMResult into the `{ usage: { inputTokens, outputTokens } }`
145
+ * shape that settleFrameworkCall expects. LangChain reports token usage on
146
+ * `llmOutput.tokenUsage` (OpenAI) or `llmOutput.usage`.
147
+ */
148
+ function normalizeUsage(output: unknown): { usage: Record<string, unknown> } | null {
149
+ const record = objectRecord(output);
150
+ const llmOutput = objectRecord(record?.llmOutput);
151
+ const tokenUsage = objectRecord(llmOutput?.tokenUsage) ?? objectRecord(llmOutput?.usage);
152
+ if (!tokenUsage) return null;
153
+ return {
154
+ usage: {
155
+ inputTokens: tokenUsage.promptTokens ?? tokenUsage.prompt_tokens ?? tokenUsage.input_tokens ?? tokenUsage.inputTokens,
156
+ outputTokens: tokenUsage.completionTokens ?? tokenUsage.completion_tokens ?? tokenUsage.output_tokens ?? tokenUsage.outputTokens,
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,36 @@
1
+ import { withSpendGuard, type OpenAIBindingOptions } from '../spend-guard';
2
+
3
+ /**
4
+ * One-line spend governance for the OpenAI SDK and the OpenAI Agents SDK.
5
+ *
6
+ * Hook point: the OpenAI client's `chat.completions.create`. `withSpendGuard`
7
+ * (the shared core binding) wraps that method so `guard.decide()` runs before
8
+ * every dispatch and can block/downgrade. Because the OpenAI Agents SDK
9
+ * (`@openai/agents`) drives the same underlying `OpenAI` client, passing the
10
+ * guarded client to the agent runner governs every model call the agent makes.
11
+ *
12
+ * This is a thin, named re-export of `withSpendGuard` so the OpenAI integration
13
+ * is discoverable at `@agentguard-run/spend/frameworks/openai`. It reuses the
14
+ * existing enforcement verbatim — no second code path.
15
+ *
16
+ * @example OpenAI SDK
17
+ * import OpenAI from 'openai';
18
+ * import { withSpendGuardOpenAI } from '@agentguard-run/spend/frameworks/openai';
19
+ *
20
+ * const client = withSpendGuardOpenAI(new OpenAI(), { policy, scope });
21
+ * await client.chat.completions.create({ model: 'gpt-5', messages });
22
+ *
23
+ * @example OpenAI Agents SDK
24
+ * import OpenAI from 'openai';
25
+ * import { Agent, run, setDefaultOpenAIClient } from '@openai/agents';
26
+ * import { withSpendGuardOpenAI } from '@agentguard-run/spend/frameworks/openai';
27
+ *
28
+ * setDefaultOpenAIClient(withSpendGuardOpenAI(new OpenAI(), { policy, scope }));
29
+ * await run(new Agent({ name: 'assistant' }), 'hello');
30
+ */
31
+ export function withSpendGuardOpenAI<TClient>(client: TClient, opts: OpenAIBindingOptions): TClient {
32
+ return withSpendGuard(client, opts) as TClient;
33
+ }
34
+
35
+ export const withOpenAISpendGuard = withSpendGuardOpenAI;
36
+ export type { OpenAIBindingOptions } from '../spend-guard';
package/src/router.ts CHANGED
@@ -11,6 +11,22 @@
11
11
 
12
12
  import { getCachedCatalog, type OpenRouterCatalog } from './openrouter-catalog';
13
13
 
14
+ // Spend-and-policy-aware model router (removes vendor risk). Re-exported here so
15
+ // the `./router` subpath exposes both the config recommender and the executor.
16
+ export {
17
+ createRouter,
18
+ RoutingExhaustedError,
19
+ type ModelRouter,
20
+ type CreateRouterConfig,
21
+ type RouterCandidate,
22
+ type RouterPolicy,
23
+ type RouterProvider,
24
+ type RouterRequest,
25
+ type RouteResult,
26
+ type RouteReason,
27
+ type RouteFallback,
28
+ } from './router-engine';
29
+
14
30
  export interface RouterCatalogModel {
15
31
  id: string;
16
32
  in_per_mtok: number;