@stigg/agent-sdk-js 0.7.7 → 0.7.9
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 +3 -2
- package/dist/bindings/client/core/wire-surface.d.ts +29 -0
- package/dist/bindings/client/core/wire-surface.js +79 -0
- package/dist/bindings/client/gateways/agentgateway/agentgateway-recipe.d.ts +33 -0
- package/dist/bindings/client/gateways/agentgateway/agentgateway-recipe.js +3 -51
- package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.js +18 -68
- package/dist/bindings/client/gateways/truefoundry/truefoundry-recipe.d.ts +35 -0
- package/dist/bindings/client/gateways/truefoundry/truefoundry-recipe.js +75 -0
- package/dist/bindings/client/gateways/truefoundry/types.d.ts +22 -0
- package/dist/bindings/client/gateways/truefoundry/types.js +2 -0
- package/dist/bindings/orchestrator/vercel-ai/middleware.d.ts +30 -3
- package/dist/bindings/orchestrator/vercel-ai/middleware.js +168 -6
- package/dist/bindings/shared/token-usage.d.ts +14 -18
- package/dist/bindings/shared/token-usage.js +16 -24
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -1
- package/dist/stigg-agent-sdk.js +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -155,6 +155,7 @@ closed, and conflating them is how an outage takes an application down.
|
|
|
155
155
|
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/bedrock.svg" width="18" alt=""> | Amazon Bedrock | <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/litellm.png" width="18" alt=""> | LiteLLM |
|
|
156
156
|
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/cloudflare.svg" width="18" alt=""> | Cloudflare Workers AI | <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/openrouter.svg" width="18" alt=""> | OpenRouter |
|
|
157
157
|
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/databricks.svg" width="18" alt=""> | Databricks | | agentgateway (incl. Solo.io Enterprise) |
|
|
158
|
+
| | TrueFoundry AI Gateway | | |
|
|
158
159
|
|
|
159
160
|
A gateway called with plain HTTP rather than through an SDK is governed the same way:
|
|
160
161
|
`stigg.wrapFetch({ baseUrl, recipe })` hands back a `fetch` that is checked and metered per
|
|
@@ -165,7 +166,7 @@ request.
|
|
|
165
166
|
| | Orchestrator | | Orchestrator |
|
|
166
167
|
| --- | --- | --- | --- |
|
|
167
168
|
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/langchain.svg" width="18" alt=""> | [LangChain](docs/langchain.md) | <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/langgraph.svg" width="18" alt=""> | [LangGraph](docs/langchain.md#a-hand-built-langgraph) |
|
|
168
|
-
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/google-adk.png" width="18" alt=""> | [Google ADK](docs/guide.md#google-adk-one-plugin-per-runner) | <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/vercel.svg" width="18" alt=""> | [Vercel AI SDK](docs/vercel-ai.md)
|
|
169
|
+
| <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/google-adk.png" width="18" alt=""> | [Google ADK](docs/guide.md#google-adk-one-plugin-per-runner) | <img src="https://raw.githubusercontent.com/stiggio/stigg-agent-sdk-ts/main/docs/logos/vercel.svg" width="18" alt=""> | [Vercel AI SDK](docs/vercel-ai.md) |
|
|
169
170
|
| | Your own harness, through `stigg.middleware()` | | |
|
|
170
171
|
|
|
171
172
|
Each binding is written against the *shape a vendor returns*, not against its SDK — so installing
|
|
@@ -187,7 +188,7 @@ rather than handing back a client that quietly meters nothing.
|
|
|
187
188
|
| Meter a gateway you operate | `client.stigg.headers()` out, `stigg.ingestFromHeaders(...)` back |
|
|
188
189
|
| Govern a LangChain / LangGraph agent graph ([guide](docs/langchain.md)) | `new StiggLangChainCallbackHandler({ stigg, agents, tools })` from `@stigg/agent-sdk-js/langchain` |
|
|
189
190
|
| Govern a Google ADK agent graph ([guide](docs/guide.md#google-adk-one-plugin-per-runner)) | `new StiggAdkPlugin({ stigg, agents, tools, stepContext })` from `@stigg/agent-sdk-js/adk`, in the Runner's `plugins` |
|
|
190
|
-
| Govern a Vercel AI SDK model or agent ([guide](docs/vercel-ai.md)) | `wrapLanguageModel({ model, middleware: stiggVercelMiddleware({ stigg, stiggContext }) })`, a call's own in `providerOptions.stigg` |
|
|
191
|
+
| Govern a Vercel AI SDK model or agent ([guide](docs/vercel-ai.md)) | `wrapLanguageModel({ model, middleware: stiggVercelMiddleware({ stigg, stiggContext, tools }) })`, a call's own in `providerOptions.stigg`; paid tools through `middleware.tools(...)` |
|
|
191
192
|
| Govern a graph from your own harness | `stigg.middleware({ stiggContext, agents })` |
|
|
192
193
|
|
|
193
194
|
One wrapped client serves every customer and every agent: the context is an argument, given once at
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MethodRecipe, MeteredResponse } from './types';
|
|
2
|
+
/** What the response body says about the call, buffered or streamed. */
|
|
3
|
+
export type BodyFacts = {
|
|
4
|
+
model?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
};
|
|
7
|
+
/** What only the gateway knows, folded onto the wire reader's measurement. */
|
|
8
|
+
export type GatewayFacts = {
|
|
9
|
+
/** The model that actually served, reported under the recipe's model dimension. */
|
|
10
|
+
model?: string;
|
|
11
|
+
/** The upstream's completion id: the idempotency key when the wire reader stated none. */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** The gateway's own request id: the idempotency key ahead of anything the wire reader stated. */
|
|
14
|
+
requestId?: string;
|
|
15
|
+
/** Anything else the gateway states, as dimensions. */
|
|
16
|
+
dimensions?: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
export type GatewaySurfaceOptions = {
|
|
19
|
+
estimate: NonNullable<MethodRecipe['estimate']>;
|
|
20
|
+
modelDimension: string;
|
|
21
|
+
/** The gateway's facts, from the body and the response. Defaults to the body's own. */
|
|
22
|
+
facts?: (body: BodyFacts, response: MeteredResponse | undefined) => GatewayFacts;
|
|
23
|
+
/** A call nobody was charged for (a cache hit), which reports nothing at all. */
|
|
24
|
+
skip?: (response: MeteredResponse | undefined) => boolean;
|
|
25
|
+
};
|
|
26
|
+
/** A response header, with an empty value read as absent. */
|
|
27
|
+
export declare function headerOf(response: MeteredResponse | undefined, name: string): string | undefined;
|
|
28
|
+
/** One wire's surface, answered through a gateway. */
|
|
29
|
+
export declare function gatewaySurface(base: MethodRecipe, options: GatewaySurfaceOptions): MethodRecipe;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.headerOf = headerOf;
|
|
4
|
+
exports.gatewaySurface = gatewaySurface;
|
|
5
|
+
/**
|
|
6
|
+
* A provider's wire, answered by a gateway - the wrapper agentgateway, TrueFoundry and Cloudflare
|
|
7
|
+
* AI Gateway share.
|
|
8
|
+
*
|
|
9
|
+
* Each of those gateways answers in the shape of the wire it was called on, so that wire's recipe
|
|
10
|
+
* already has the right reader - and Anthropic's streamed usage in particular accumulates across
|
|
11
|
+
* two event types, which is not logic worth having a second copy of. What the gateway adds is what
|
|
12
|
+
* only it knows: the model that actually served and the id a replay repeats, plus whatever a
|
|
13
|
+
* particular gateway states in its own headers. This file folds those onto the wire reader's
|
|
14
|
+
* measurement; each recipe supplies the facts.
|
|
15
|
+
*/
|
|
16
|
+
const meter_stream_1 = require("../../shared/meter-stream");
|
|
17
|
+
const types_1 = require("../../../types");
|
|
18
|
+
/** A response header, with an empty value read as absent. */
|
|
19
|
+
function headerOf(response, name) {
|
|
20
|
+
const raw = response?.headers?.get(name);
|
|
21
|
+
return raw == null || raw === '' ? undefined : raw;
|
|
22
|
+
}
|
|
23
|
+
function withGatewayFacts(measured, facts, modelDimension) {
|
|
24
|
+
const dimensions = {
|
|
25
|
+
...measured.dimensions,
|
|
26
|
+
...facts.dimensions,
|
|
27
|
+
};
|
|
28
|
+
if (facts.model)
|
|
29
|
+
dimensions[modelDimension] = facts.model;
|
|
30
|
+
return {
|
|
31
|
+
...measured,
|
|
32
|
+
dimensions: Object.keys(dimensions).length > 0 ? dimensions : undefined,
|
|
33
|
+
idempotencyKey: facts.requestId ?? measured.idempotencyKey ?? facts.id,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A non-empty string, or absent - an empty id would key every such call as one, the stream tap's rule too. */
|
|
37
|
+
const stringOr = (value) => (typeof value === 'string' && value ? value : undefined);
|
|
38
|
+
/** One wire's surface, answered through a gateway. */
|
|
39
|
+
function gatewaySurface(base, options) {
|
|
40
|
+
const { estimate, modelDimension, skip } = options;
|
|
41
|
+
const facts = options.facts ?? ((body) => body);
|
|
42
|
+
return {
|
|
43
|
+
// Spread first so anything the base declares and this wrapper does not know about - like
|
|
44
|
+
// `contextArg` - survives being wrapped.
|
|
45
|
+
...base,
|
|
46
|
+
estimate,
|
|
47
|
+
extractUsage: (result, response) => {
|
|
48
|
+
// Checked before the body is read, because the body of a cache hit looks like a real call.
|
|
49
|
+
if (skip?.(response))
|
|
50
|
+
return undefined;
|
|
51
|
+
const measured = base.extractUsage?.(result, response);
|
|
52
|
+
if (measured === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const body = { model: stringOr(result?.model), id: stringOr(result?.id) };
|
|
55
|
+
return withGatewayFacts((0, types_1.toMeasurement)(measured), facts(body, response), modelDimension);
|
|
56
|
+
},
|
|
57
|
+
attachStream: base.attachStream
|
|
58
|
+
? (stream, ingest, response) => {
|
|
59
|
+
// Nothing attached at all: the consumer's stream is left untouched and nothing reported.
|
|
60
|
+
if (skip?.(response))
|
|
61
|
+
return;
|
|
62
|
+
// A second tap for the body's model and id - the wire readers carry out counters and
|
|
63
|
+
// nothing else. Each wire states them in its own envelope: Anthropic on
|
|
64
|
+
// `message_start.message`, the Responses API on `response`, Chat Completions at the
|
|
65
|
+
// chunk root. `meterStream` captures what it patches before patching, so two observers
|
|
66
|
+
// each see every chunk exactly once.
|
|
67
|
+
const seen = {};
|
|
68
|
+
(0, meter_stream_1.meterStream)(stream, (chunk) => {
|
|
69
|
+
const envelope = chunk?.message ?? chunk?.response ?? chunk;
|
|
70
|
+
if (typeof envelope?.model === 'string' && envelope.model)
|
|
71
|
+
seen.model = envelope.model;
|
|
72
|
+
if (typeof envelope?.id === 'string' && envelope.id)
|
|
73
|
+
seen.id = envelope.id;
|
|
74
|
+
}, () => { });
|
|
75
|
+
base.attachStream(stream, (measurement) => ingest(withGatewayFacts((0, types_1.toMeasurement)(measurement), facts({ ...seen }, response), modelDimension)), response);
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agentgateway recipe - the open-source AI gateway (and the builds on it: Solo.io's enterprise
|
|
3
|
+
* edition, Gloo AI Gateway), a gateway you call that states no price.
|
|
4
|
+
*
|
|
5
|
+
* Its OpenAI routes fit `openAIRecipe` by shape and its Anthropic route fits `anthropicRecipe`, so
|
|
6
|
+
* the recipe has to justify itself, and it does on two counts:
|
|
7
|
+
*
|
|
8
|
+
* - **A vendor name of its own.** Without this, traffic through the gateway meters as `openai.*`
|
|
9
|
+
* or `anthropic.*` - indistinguishable from a direct call, so no meter and no per-provider
|
|
10
|
+
* budget can tell the gateway's traffic apart. Worse on the Anthropic wire: `anthropicRecipe`
|
|
11
|
+
* has no base-URL guard, so an `Anthropic` client pointed here would be claimed by it and
|
|
12
|
+
* reported as direct Anthropic traffic.
|
|
13
|
+
* - **The model that actually served.** The gateway can rewrite the model a request named - an
|
|
14
|
+
* alias, a provider prefix, a failover priority group - and the response body names the one
|
|
15
|
+
* that answered. The bare recipes drop it; this one attributes it.
|
|
16
|
+
*
|
|
17
|
+
* One vendor across every wire, because the gateway is one deployment whichever client reached it,
|
|
18
|
+
* and the wire is already in the event name (`agentgateway.chat.completions.create` against
|
|
19
|
+
* `agentgateway.messages.create`). The gateway translates where it needs to, so each route reports
|
|
20
|
+
* in the vocabulary of the wire it answered on - OpenAI's names on the OpenAI routes, Anthropic's
|
|
21
|
+
* on Messages - and `metrics` is the union, which is what `metrics` is defined to be.
|
|
22
|
+
*
|
|
23
|
+
* No `cost`, and that is the defining fact rather than an oversight. The gateway is self-hosted and
|
|
24
|
+
* states no price on any response; whatever the upstream charged is invoiced by the upstream, out
|
|
25
|
+
* of band. Never declare a metric the platform cannot report.
|
|
26
|
+
*
|
|
27
|
+
* Self-hosted, so there is no public hostname to recognise. The base URL handed to
|
|
28
|
+
* `agentgatewayRecipe({ baseUrl })` is the reliable discriminator; without one the matcher falls
|
|
29
|
+
* back to a guess on the hostname - see {@link isAgentgateway} for what it claims.
|
|
30
|
+
*
|
|
31
|
+
* The same surfaces govern raw HTTP too: `stigg.wrapFetch` routes a request by its path to the
|
|
32
|
+
* method here that names it, so a `fetch` to the gateway is metered by exactly these readers.
|
|
33
|
+
*/
|
|
1
34
|
import { AGENTGATEWAY_METRICS } from '../../../shared/token-usage';
|
|
2
35
|
import type { AgentgatewayRecipeOptions } from './types';
|
|
3
36
|
import type { ClientRecipe, MetricsFor, MetricsOf } from '../../core/types';
|
|
@@ -34,12 +34,11 @@ exports.agentgatewayRecipe = agentgatewayRecipe;
|
|
|
34
34
|
* The same surfaces govern raw HTTP too: `stigg.wrapFetch` routes a request by its path to the
|
|
35
35
|
* method here that names it, so a `fetch` to the gateway is metered by exactly these readers.
|
|
36
36
|
*/
|
|
37
|
-
const meter_stream_1 = require("../../../shared/meter-stream");
|
|
38
37
|
const token_usage_1 = require("../../../shared/token-usage");
|
|
39
38
|
const base_url_1 = require("../../../shared/base-url");
|
|
40
39
|
const openai_recipe_1 = require("../../providers/openai/openai-recipe");
|
|
41
40
|
const anthropic_recipe_1 = require("../../providers/anthropic/anthropic-recipe");
|
|
42
|
-
const
|
|
41
|
+
const wire_surface_1 = require("../../core/wire-surface");
|
|
43
42
|
/**
|
|
44
43
|
* The name that identifies the gateway in an ordinary deployment: the Kubernetes service or
|
|
45
44
|
* namespace (`agentgateway-proxy.agentgateway-system.svc`) or a hostname an operator chose to say
|
|
@@ -54,58 +53,11 @@ const types_1 = require("../../../../types");
|
|
|
54
53
|
* and out of the meters counting them. Solo's and Gloo's builds name the gateway by `baseUrl`.
|
|
55
54
|
*/
|
|
56
55
|
const HOST_LABEL = 'agentgateway';
|
|
57
|
-
/** What only the gateway knows, folded onto whatever the wire's own reader produced. */
|
|
58
|
-
function withGatewayFacts(measured, result, modelDimension) {
|
|
59
|
-
const served = typeof result?.model === 'string' ? result.model : undefined;
|
|
60
|
-
return {
|
|
61
|
-
...measured,
|
|
62
|
-
dimensions: served
|
|
63
|
-
? { ...measured.dimensions, [modelDimension]: served }
|
|
64
|
-
: measured.dimensions,
|
|
65
|
-
// The upstream's completion id, carried back through the gateway. It is what a replayed
|
|
66
|
-
// response repeats, which a local step counter cannot be.
|
|
67
|
-
idempotencyKey: measured.idempotencyKey ?? (typeof result?.id === 'string' ? result.id : undefined),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
56
|
function agentgatewayRecipe(options = {}) {
|
|
71
57
|
const estimate = options.estimate ?? (() => 1);
|
|
72
58
|
const modelDimension = options.modelDimension ?? 'model';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
* The wire recipes' own extractors are reused rather than reimplemented: the gateway answers in
|
|
77
|
-
* that wire's shape, so that wire's reader is by definition the right one - and Anthropic's
|
|
78
|
-
* streamed usage in particular accumulates across two event types, which is not logic worth
|
|
79
|
-
* having a second copy of.
|
|
80
|
-
*/
|
|
81
|
-
const surfaceOf = (base) => ({
|
|
82
|
-
// Spread first so anything the base declares and this wrapper does not know about - like
|
|
83
|
-
// `contextArg` - survives being wrapped.
|
|
84
|
-
...base,
|
|
85
|
-
estimate,
|
|
86
|
-
extractUsage: (result, response) => {
|
|
87
|
-
const measured = base.extractUsage?.(result, response);
|
|
88
|
-
return measured === undefined ? undefined : withGatewayFacts((0, types_1.toMeasurement)(measured), result, modelDimension);
|
|
89
|
-
},
|
|
90
|
-
attachStream: base.attachStream
|
|
91
|
-
? (stream, ingest, response) => {
|
|
92
|
-
// A second tap, purely for attribution: the wire readers carry out counters and nothing
|
|
93
|
-
// else. Each wire states the model in its own envelope - Anthropic on
|
|
94
|
-
// `message_start.message`, the Responses API on `response`, Chat Completions at the chunk
|
|
95
|
-
// root. `meterStream` captures what it patches before patching, so two observers each
|
|
96
|
-
// see every chunk exactly once.
|
|
97
|
-
let seen = {};
|
|
98
|
-
(0, meter_stream_1.meterStream)(stream, (chunk) => {
|
|
99
|
-
const envelope = chunk?.message ?? chunk?.response ?? chunk;
|
|
100
|
-
if (envelope?.model)
|
|
101
|
-
seen = { ...seen, model: envelope.model };
|
|
102
|
-
if (envelope?.id)
|
|
103
|
-
seen = { ...seen, id: envelope.id };
|
|
104
|
-
}, () => { });
|
|
105
|
-
base.attachStream(stream, (measurement) => ingest(withGatewayFacts((0, types_1.toMeasurement)(measurement), seen, modelDimension)), response);
|
|
106
|
-
}
|
|
107
|
-
: undefined,
|
|
108
|
-
});
|
|
59
|
+
// The gateway answers in each wire's shape, and the body's model and id are its facts.
|
|
60
|
+
const surfaceOf = (base) => (0, wire_surface_1.gatewaySurface)(base, { estimate, modelDimension });
|
|
109
61
|
return {
|
|
110
62
|
vendor: 'agentgateway',
|
|
111
63
|
metrics: token_usage_1.AGENTGATEWAY_METRICS,
|
|
@@ -32,7 +32,7 @@ const base_url_1 = require("../../../shared/base-url");
|
|
|
32
32
|
const registry_1 = require("../../core/registry");
|
|
33
33
|
const openai_recipe_1 = require("../../providers/openai/openai-recipe");
|
|
34
34
|
const anthropic_recipe_1 = require("../../providers/anthropic/anthropic-recipe");
|
|
35
|
-
const
|
|
35
|
+
const wire_surface_1 = require("../../core/wire-surface");
|
|
36
36
|
/** Workers AI, both of its routes. */
|
|
37
37
|
const CLOUDFLARE_API_HOST = 'api.cloudflare.com';
|
|
38
38
|
/** AI Gateway's public endpoint. A custom domain needs `options.baseUrl` instead. */
|
|
@@ -47,10 +47,6 @@ const ROUTED_MODEL_HEADER = 'cf-aig-routed-model';
|
|
|
47
47
|
const STEP_HEADER = 'cf-aig-step';
|
|
48
48
|
/** The gateway's own request id, stable across its retries - unlike a local step counter. */
|
|
49
49
|
const REQUEST_ID_HEADER = 'cf-aig-request-id';
|
|
50
|
-
const headerOf = (response, name) => {
|
|
51
|
-
const raw = response?.headers?.get(name);
|
|
52
|
-
return raw == null || raw === '' ? undefined : raw;
|
|
53
|
-
};
|
|
54
50
|
/**
|
|
55
51
|
* Whether the gateway served this from its cache.
|
|
56
52
|
*
|
|
@@ -62,28 +58,26 @@ const headerOf = (response, name) => {
|
|
|
62
58
|
* Absent on the Workers AI routes, which have no cache in front of them - so this reads `false`
|
|
63
59
|
* there and the surfaces behave as ordinary metered calls.
|
|
64
60
|
*/
|
|
65
|
-
const isCacheHit = (response) => headerOf(response, CACHE_STATUS_HEADER)?.toUpperCase() === 'HIT';
|
|
66
|
-
/** What only the gateway knows,
|
|
67
|
-
function
|
|
68
|
-
const dimensions = {
|
|
69
|
-
// The model that actually served: the router's own word for it, falling back to whatever the
|
|
70
|
-
// forwarded body said. Anthropic's Messages body carries `model` too, so this works on both wires.
|
|
71
|
-
const served = headerOf(response, ROUTED_MODEL_HEADER) ?? (typeof result?.model === 'string' ? result.model : undefined);
|
|
72
|
-
if (served)
|
|
73
|
-
dimensions[modelDimension] = served;
|
|
61
|
+
const isCacheHit = (response) => (0, wire_surface_1.headerOf)(response, CACHE_STATUS_HEADER)?.toUpperCase() === 'HIT';
|
|
62
|
+
/** What only the gateway knows, from its headers, with the forwarded body as the fallback. */
|
|
63
|
+
function gatewayFacts(body, response) {
|
|
64
|
+
const dimensions = {};
|
|
74
65
|
// The substitute for a cost metric: the key to the gateway's own record of what this call cost.
|
|
75
|
-
const logId = headerOf(response, LOG_ID_HEADER);
|
|
66
|
+
const logId = (0, wire_surface_1.headerOf)(response, LOG_ID_HEADER);
|
|
76
67
|
if (logId)
|
|
77
68
|
dimensions.cf_aig_log_id = logId;
|
|
78
69
|
// Only when a fallback actually happened. Step `0` is the ordinary case and tagging every event
|
|
79
70
|
// with it would add a dimension to all of them for no information.
|
|
80
|
-
const step = headerOf(response, STEP_HEADER);
|
|
71
|
+
const step = (0, wire_surface_1.headerOf)(response, STEP_HEADER);
|
|
81
72
|
if (step !== undefined && step !== '0')
|
|
82
73
|
dimensions.cf_aig_step = step;
|
|
83
74
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
// The model that actually served: the router's own word for it, falling back to whatever the
|
|
76
|
+
// forwarded body said. Anthropic's Messages body carries `model` too, so this works on both wires.
|
|
77
|
+
model: (0, wire_surface_1.headerOf)(response, ROUTED_MODEL_HEADER) ?? body.model,
|
|
78
|
+
id: body.id,
|
|
79
|
+
requestId: (0, wire_surface_1.headerOf)(response, REQUEST_ID_HEADER),
|
|
80
|
+
dimensions,
|
|
87
81
|
};
|
|
88
82
|
}
|
|
89
83
|
/** The Workers AI reader: the three counters it states, under its own (OpenAI's) names. */
|
|
@@ -106,57 +100,13 @@ function cloudflareRecipe(options = {}) {
|
|
|
106
100
|
const estimate = options.estimate ?? (() => 1);
|
|
107
101
|
const modelDimension = options.modelDimension ?? 'model';
|
|
108
102
|
/**
|
|
109
|
-
* One upstream surface, wrapped.
|
|
103
|
+
* One upstream surface, wrapped. Off the gateway the wrapper is inert: no `cf-aig-*` header is
|
|
104
|
+
* present, so no cache hit is seen and no gateway dimension is added.
|
|
110
105
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* the right one - and Anthropic's streamed usage in particular accumulates across two event
|
|
114
|
-
* types, which is not logic worth having a second copy of. Off the gateway the wrapper is inert:
|
|
115
|
-
* no `cf-aig-*` header is present, so no cache hit is seen and no gateway dimension is added.
|
|
106
|
+
* A cache hit reports nothing, buffered or streamed: nobody was invoiced for it, and the cache
|
|
107
|
+
* header arrives before any token does.
|
|
116
108
|
*/
|
|
117
|
-
const surfaceOf = (base) => ({
|
|
118
|
-
// Spread first so anything the base declares and this wrapper does not know about - like
|
|
119
|
-
// `contextArg`, which says where `stiggContext` rides - survives being wrapped. Rebuilding
|
|
120
|
-
// the object field by field silently dropped it.
|
|
121
|
-
...base,
|
|
122
|
-
path: base.path,
|
|
123
|
-
estimate,
|
|
124
|
-
extractUsage: (result, response) => {
|
|
125
|
-
// Nobody was invoiced for a cache hit, so nothing is reported for one. Checked before the
|
|
126
|
-
// body is read at all, because the body of a hit looks exactly like the body of a real call.
|
|
127
|
-
if (isCacheHit(response))
|
|
128
|
-
return undefined;
|
|
129
|
-
const measured = base.extractUsage?.(result, response);
|
|
130
|
-
return measured === undefined
|
|
131
|
-
? undefined
|
|
132
|
-
: withGatewayFacts((0, types_1.toMeasurement)(measured), result, response, modelDimension);
|
|
133
|
-
},
|
|
134
|
-
attachStream: base.attachStream
|
|
135
|
-
? (stream, ingest, response) => {
|
|
136
|
-
// A cached reply can be streamed back too, and the cache header arrives in time to say so.
|
|
137
|
-
// Nothing is attached at all in that case: the consumer's stream is left untouched and no
|
|
138
|
-
// measurement is produced, which is what a free call should report.
|
|
139
|
-
if (isCacheHit(response))
|
|
140
|
-
return;
|
|
141
|
-
// A second tap, purely for attribution. The upstream recipes' stream readers carry out
|
|
142
|
-
// counters and nothing else, so without this the streamed path had no `result` to fall
|
|
143
|
-
// back on and a turn with no `cf-aig-routed-model` header lost its model entirely -
|
|
144
|
-
// where the buffered path reads it straight off the body. Each wire states it in its own
|
|
145
|
-
// envelope: Anthropic on `message_start.message`, the Responses API on `response`, Chat
|
|
146
|
-
// Completions at the chunk root. `meterStream` captures what it patches before patching,
|
|
147
|
-
// so stacking two observers is safe and each sees every chunk exactly once.
|
|
148
|
-
let seen = {};
|
|
149
|
-
(0, meter_stream_1.meterStream)(stream, (chunk) => {
|
|
150
|
-
const envelope = chunk?.message ?? chunk?.response ?? chunk;
|
|
151
|
-
if (envelope?.model)
|
|
152
|
-
seen = { ...seen, model: envelope.model };
|
|
153
|
-
if (envelope?.id)
|
|
154
|
-
seen = { ...seen, id: envelope.id };
|
|
155
|
-
}, () => { });
|
|
156
|
-
base.attachStream(stream, (measurement) => ingest(withGatewayFacts((0, types_1.toMeasurement)(measurement), seen, response, modelDimension)), response);
|
|
157
|
-
}
|
|
158
|
-
: undefined,
|
|
159
|
-
});
|
|
109
|
+
const surfaceOf = (base) => (0, wire_surface_1.gatewaySurface)(base, { estimate, modelDimension, facts: gatewayFacts, skip: isCacheHit });
|
|
160
110
|
/**
|
|
161
111
|
* Chat Completions, which both products answer on - Workers AI's OpenAI-compatible route and the
|
|
162
112
|
* gateway's `/openai` one.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TrueFoundry recipe - TrueFoundry's AI Gateway, hosted at `gateway.truefoundry.ai` or
|
|
3
|
+
* self-hosted in your cluster, a gateway you call that states no price.
|
|
4
|
+
*
|
|
5
|
+
* Apps reach it by pointing an existing `OpenAI` or `Anthropic` client at the gateway's base URL
|
|
6
|
+
* with a TrueFoundry key, and naming models as `provider_account/model` (`openai-main/gpt-5-mini`).
|
|
7
|
+
* Both wires fit `openAIRecipe` and `anthropicRecipe` by shape, so the recipe earns its place the
|
|
8
|
+
* way agentgateway's does:
|
|
9
|
+
*
|
|
10
|
+
* - **A vendor name of its own.** Without this, gateway traffic meters as `openai.*` or
|
|
11
|
+
* `anthropic.*`, indistinguishable from a direct call. `anthropicRecipe` has no base-URL guard,
|
|
12
|
+
* so an `Anthropic` client pointed here would be claimed as direct Anthropic traffic.
|
|
13
|
+
* - **The model that actually served.** TrueFoundry load-balances and falls back between provider
|
|
14
|
+
* accounts, and names the one that answered in the `x-tfy-resolved-model` response header.
|
|
15
|
+
* Headers go on the wire before the body, so a streamed call has it too. The body's `model` is
|
|
16
|
+
* the fallback when the header is absent.
|
|
17
|
+
*
|
|
18
|
+
* No `cost`. TrueFoundry tracks spend in its own dashboard and puts no price on any response; the
|
|
19
|
+
* provider accounts behind it are the caller's own keys, so the upstream invoices the tokens.
|
|
20
|
+
*
|
|
21
|
+
* Not covered: TrueFoundry's Gemini (`/gemini/<account>/proxy`), Bedrock (`/bedrock/proxy`) and
|
|
22
|
+
* Azure OpenAI (`/azure-openai`) passthroughs. Each speaks its own provider's SDK and shape.
|
|
23
|
+
*
|
|
24
|
+
* `stigg.wrapFetch` governs raw HTTP to the gateway through these same methods.
|
|
25
|
+
*/
|
|
26
|
+
import { TRUEFOUNDRY_METRICS } from '../../../shared/token-usage';
|
|
27
|
+
import type { TrueFoundryRecipeOptions } from './types';
|
|
28
|
+
import type { ClientRecipe, MetricsFor, MetricsOf } from '../../core/types';
|
|
29
|
+
type TrueFoundryMetric = (typeof TRUEFOUNDRY_METRICS)[number];
|
|
30
|
+
export declare function truefoundryRecipe(options?: TrueFoundryRecipeOptions): ClientRecipe<TrueFoundryMetric>;
|
|
31
|
+
/** One of the metric names TrueFoundry can report, across every wire it serves. */
|
|
32
|
+
export type TrueFoundryMetricName = MetricsOf<ReturnType<typeof truefoundryRecipe>>;
|
|
33
|
+
/** What a mapper reads off a call through TrueFoundry: those names, each possibly absent. */
|
|
34
|
+
export type TrueFoundryMetrics = MetricsFor<ReturnType<typeof truefoundryRecipe>>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.truefoundryRecipe = truefoundryRecipe;
|
|
4
|
+
/**
|
|
5
|
+
* The TrueFoundry recipe - TrueFoundry's AI Gateway, hosted at `gateway.truefoundry.ai` or
|
|
6
|
+
* self-hosted in your cluster, a gateway you call that states no price.
|
|
7
|
+
*
|
|
8
|
+
* Apps reach it by pointing an existing `OpenAI` or `Anthropic` client at the gateway's base URL
|
|
9
|
+
* with a TrueFoundry key, and naming models as `provider_account/model` (`openai-main/gpt-5-mini`).
|
|
10
|
+
* Both wires fit `openAIRecipe` and `anthropicRecipe` by shape, so the recipe earns its place the
|
|
11
|
+
* way agentgateway's does:
|
|
12
|
+
*
|
|
13
|
+
* - **A vendor name of its own.** Without this, gateway traffic meters as `openai.*` or
|
|
14
|
+
* `anthropic.*`, indistinguishable from a direct call. `anthropicRecipe` has no base-URL guard,
|
|
15
|
+
* so an `Anthropic` client pointed here would be claimed as direct Anthropic traffic.
|
|
16
|
+
* - **The model that actually served.** TrueFoundry load-balances and falls back between provider
|
|
17
|
+
* accounts, and names the one that answered in the `x-tfy-resolved-model` response header.
|
|
18
|
+
* Headers go on the wire before the body, so a streamed call has it too. The body's `model` is
|
|
19
|
+
* the fallback when the header is absent.
|
|
20
|
+
*
|
|
21
|
+
* No `cost`. TrueFoundry tracks spend in its own dashboard and puts no price on any response; the
|
|
22
|
+
* provider accounts behind it are the caller's own keys, so the upstream invoices the tokens.
|
|
23
|
+
*
|
|
24
|
+
* Not covered: TrueFoundry's Gemini (`/gemini/<account>/proxy`), Bedrock (`/bedrock/proxy`) and
|
|
25
|
+
* Azure OpenAI (`/azure-openai`) passthroughs. Each speaks its own provider's SDK and shape.
|
|
26
|
+
*
|
|
27
|
+
* `stigg.wrapFetch` governs raw HTTP to the gateway through these same methods.
|
|
28
|
+
*/
|
|
29
|
+
const token_usage_1 = require("../../../shared/token-usage");
|
|
30
|
+
const base_url_1 = require("../../../shared/base-url");
|
|
31
|
+
const openai_recipe_1 = require("../../providers/openai/openai-recipe");
|
|
32
|
+
const anthropic_recipe_1 = require("../../providers/anthropic/anthropic-recipe");
|
|
33
|
+
const wire_surface_1 = require("../../core/wire-surface");
|
|
34
|
+
/** TrueFoundry's own domains: the hosted gateway, and the tenant control planes that serve one. */
|
|
35
|
+
const TRUEFOUNDRY_HOSTS = ['truefoundry.ai', 'truefoundry.cloud', 'truefoundry.com'];
|
|
36
|
+
/** Which model answered, after TrueFoundry's load balancing or fallback. */
|
|
37
|
+
const RESOLVED_MODEL_HEADER = 'x-tfy-resolved-model';
|
|
38
|
+
function truefoundryRecipe(options = {}) {
|
|
39
|
+
const estimate = options.estimate ?? (() => 1);
|
|
40
|
+
const modelDimension = options.modelDimension ?? 'model';
|
|
41
|
+
// The gateway answers in each wire's shape. The header names the model that served, ahead of
|
|
42
|
+
// the body's, and is on the wire before a streamed body is.
|
|
43
|
+
const surfaceOf = (base) => (0, wire_surface_1.gatewaySurface)(base, {
|
|
44
|
+
estimate,
|
|
45
|
+
modelDimension,
|
|
46
|
+
facts: (body, response) => ({ ...body, model: (0, wire_surface_1.headerOf)(response, RESOLVED_MODEL_HEADER) ?? body.model }),
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
vendor: 'truefoundry',
|
|
50
|
+
metrics: token_usage_1.TRUEFOUNDRY_METRICS,
|
|
51
|
+
totalTokensMetric: token_usage_1.TOTAL_TOKENS,
|
|
52
|
+
matches: (client) => isTrueFoundry(client, options.baseUrl) && (openai_recipe_1.openAIRecipe.matches(client) || anthropic_recipe_1.anthropicRecipe.matches(client)),
|
|
53
|
+
// Every wire it serves. This recipe is matched ahead of both bare ones, so a surface left out
|
|
54
|
+
// here would be ungated rather than metered by them.
|
|
55
|
+
methods: [...openai_recipe_1.openAIRecipe.methods.map(surfaceOf), ...anthropic_recipe_1.anthropicRecipe.methods.map(surfaceOf)],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Given a `baseUrl`, the endpoint is compared one path segment at a time, so `.../v1` cannot claim
|
|
60
|
+
* `.../v10`, and TrueFoundry's own domains are no longer claimed. Without one, only TrueFoundry's own
|
|
61
|
+
* domains and their subdomains are claimed - a self-hosted install under your hostname needs the
|
|
62
|
+
* `baseUrl`.
|
|
63
|
+
*
|
|
64
|
+
* That claims every OpenAI- or Anthropic-shaped client on those domains, not only the gateway - a
|
|
65
|
+
* model TrueFoundry serves there directly reports under `truefoundry.*` too. Name
|
|
66
|
+
* `recipe: openAIRecipe` (or `anthropicRecipe`) at `wrap` to settle it.
|
|
67
|
+
*/
|
|
68
|
+
function isTrueFoundry(client, baseUrl) {
|
|
69
|
+
const clientBase = (0, base_url_1.baseUrlOf)(client);
|
|
70
|
+
if (clientBase === undefined)
|
|
71
|
+
return false;
|
|
72
|
+
if (baseUrl)
|
|
73
|
+
return (0, base_url_1.withinBaseUrl)(baseUrl, clientBase);
|
|
74
|
+
return (0, base_url_1.matchesAnyHost)(clientBase, TRUEFOUNDRY_HOSTS);
|
|
75
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type TrueFoundryRecipeOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* How much to ask permission for before the call, in whatever unit `meter` bills.
|
|
4
|
+
*
|
|
5
|
+
* The default asks for 1 - "is there any budget left at all?" - and lets the ingest afterwards
|
|
6
|
+
* settle the balance. No `unitsPer…` beside it: TrueFoundry puts no price on any response, so a
|
|
7
|
+
* run here bills token counters, which arrive whole.
|
|
8
|
+
*/
|
|
9
|
+
estimate?: (params: any) => number;
|
|
10
|
+
/**
|
|
11
|
+
* Dimension the model that actually served is attributed to, so routing drift is visible in
|
|
12
|
+
* Stigg. TrueFoundry load-balances and falls back between provider accounts, and says which one
|
|
13
|
+
* answered in the `x-tfy-resolved-model` response header.
|
|
14
|
+
*/
|
|
15
|
+
modelDimension?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The gateway this recipe is for, when it is not TrueFoundry's hosted one. A self-hosted install
|
|
18
|
+
* runs under your own hostname, so give it the same base URL the client was built with and
|
|
19
|
+
* `matches` compares against exactly that. Without it only TrueFoundry's own domains are claimed.
|
|
20
|
+
*/
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
};
|
|
@@ -9,16 +9,26 @@
|
|
|
9
9
|
* bindings. A call's own `stiggContext` rides `providerOptions.stigg`, and is taken out before the
|
|
10
10
|
* provider sees the call. Nothing here imports `ai`: a middleware is a plain object. See
|
|
11
11
|
* docs/vercel-ai.md.
|
|
12
|
+
*
|
|
13
|
+
* A model middleware never sees a tool run, so the paid tools are governed by `tools()`, which wraps
|
|
14
|
+
* each one's `execute`. A tool call is governed as the model step that asked for it.
|
|
12
15
|
*/
|
|
13
16
|
import type { StiggAgentSDK } from '../../../stigg-agent-sdk';
|
|
14
17
|
import type { SessionHooks } from '../../../core/session';
|
|
15
|
-
import type { ClientContext, GovernedAgentMiddleware, Measurement } from '../../../types';
|
|
18
|
+
import type { ClientContext, GovernedAgentMiddleware, Measurement, MiddlewareTools } from '../../../types';
|
|
19
|
+
import { type ModelUsage } from './usage';
|
|
16
20
|
export { VERCEL_AI_METRICS, measurementOf } from './usage';
|
|
17
21
|
export type { VercelAIMetric, VercelAIMetrics } from './usage';
|
|
18
22
|
export type StiggVercelMiddlewareOptions = {
|
|
19
23
|
stigg: StiggAgentSDK;
|
|
20
24
|
/** Every call's context - typically the agent's `agentId` and what it gates on. A call's `providerOptions.stigg` is laid over it. */
|
|
21
25
|
stiggContext?: ClientContext;
|
|
26
|
+
/**
|
|
27
|
+
* The paid tools, by tool name - `'*'` for every tool not named, `false` to exempt one. Governed
|
|
28
|
+
* once wrapped by `tools()`. A sub-agent run inside a tool is metered by its own model calls, so
|
|
29
|
+
* give it `false` under `'*'`.
|
|
30
|
+
*/
|
|
31
|
+
tools?: MiddlewareTools;
|
|
22
32
|
hooks?: SessionHooks;
|
|
23
33
|
};
|
|
24
34
|
/**
|
|
@@ -40,13 +50,30 @@ export type StiggVercelMiddleware = {
|
|
|
40
50
|
params: any;
|
|
41
51
|
model?: any;
|
|
42
52
|
}): Promise<any>;
|
|
53
|
+
/**
|
|
54
|
+
* The same tools, each paid one's `execute` wrapped: checked before it runs, and reported after it
|
|
55
|
+
* succeeds. A refused tool does not run; it fails with a message that names the tool and why, and
|
|
56
|
+
* `stigg.denial(error)` returns the decision. Tools with no `execute` are returned as they are.
|
|
57
|
+
*/
|
|
58
|
+
tools<T extends Record<string, any>>(tools: T): T;
|
|
43
59
|
/** What this middleware did, and what is still in flight. */
|
|
44
60
|
readonly stigg: GovernedAgentMiddleware['stigg'];
|
|
45
61
|
};
|
|
46
|
-
export declare function stiggVercelMiddleware({ stigg, stiggContext, hooks, }: StiggVercelMiddlewareOptions): StiggVercelMiddleware;
|
|
62
|
+
export declare function stiggVercelMiddleware({ stigg, stiggContext, tools: toolConfig, hooks, }: StiggVercelMiddlewareOptions): StiggVercelMiddleware;
|
|
63
|
+
type StreamPart = {
|
|
64
|
+
type?: string;
|
|
65
|
+
id?: string;
|
|
66
|
+
modelId?: unknown;
|
|
67
|
+
delta?: unknown;
|
|
68
|
+
usage?: ModelUsage;
|
|
69
|
+
};
|
|
47
70
|
/**
|
|
48
71
|
* Calls `done` once, when the stream ends: read to the end, failed, cancelled, or stopped by the
|
|
49
72
|
* call's `abortSignal` - which is how the AI SDK stops a stream, without cancelling it.
|
|
50
73
|
* ponytail: a reader that stops with no `abortSignal` is not reported; nothing tells this layer.
|
|
51
74
|
*/
|
|
52
|
-
export declare function observeStream(result: any, signal: AbortSignal | undefined, done: (measurement: Measurement | undefined, modelId?: string) => void
|
|
75
|
+
export declare function observeStream(result: any, signal: AbortSignal | undefined, done: (measurement: Measurement | undefined, modelId?: string) => void,
|
|
76
|
+
/** Each part, before the AI SDK reads it - so a tool call is known before its tool can start. */
|
|
77
|
+
onPart?: (part: StreamPart & {
|
|
78
|
+
toolCallId?: unknown;
|
|
79
|
+
}) => void): void;
|
|
@@ -3,17 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.measurementOf = exports.VERCEL_AI_METRICS = void 0;
|
|
4
4
|
exports.stiggVercelMiddleware = stiggVercelMiddleware;
|
|
5
5
|
exports.observeStream = observeStream;
|
|
6
|
+
const errors_1 = require("../../../errors");
|
|
7
|
+
const hooks_1 = require("../../../core/hooks");
|
|
8
|
+
const agent_middleware_1 = require("../core/agent-middleware");
|
|
6
9
|
const usage_1 = require("./usage");
|
|
7
10
|
var usage_2 = require("./usage");
|
|
8
11
|
Object.defineProperty(exports, "VERCEL_AI_METRICS", { enumerable: true, get: function () { return usage_2.VERCEL_AI_METRICS; } });
|
|
9
12
|
Object.defineProperty(exports, "measurementOf", { enumerable: true, get: function () { return usage_2.measurementOf; } });
|
|
10
13
|
const MISSING_CUSTOMER = 'Name it on every call as `providerOptions: { stigg: { customerId } }`, ' +
|
|
11
14
|
'or give the middleware a default in its `stiggContext: { customerId }`.';
|
|
12
|
-
|
|
15
|
+
/** How many asked-for tool calls are remembered. One that never runs - a client-side tool - is dropped past it. */
|
|
16
|
+
const ISSUED_CALLS_LIMIT = 1_000;
|
|
17
|
+
function stiggVercelMiddleware({ stigg, stiggContext, tools: toolConfig, hooks, }) {
|
|
13
18
|
if (!stigg || typeof stigg.middleware !== 'function') {
|
|
14
19
|
throw new Error('stiggVercelMiddleware needs `stigg`: the StiggAgentSDK instance to govern through.');
|
|
15
20
|
}
|
|
16
|
-
const governance = stigg.middleware({ stiggContext, hooks, missingCustomer: MISSING_CUSTOMER });
|
|
21
|
+
const governance = stigg.middleware({ stiggContext, tools: toolConfig, hooks, missingCustomer: MISSING_CUSTOMER });
|
|
17
22
|
// `doGenerate()` takes no arguments: the model is called with what `transformParams` returned.
|
|
18
23
|
// So the context comes out there, and is found again by that params object.
|
|
19
24
|
const contexts = new WeakMap();
|
|
@@ -27,11 +32,128 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
27
32
|
// of the same customer included - never queue behind it. A call with no context of its own falls
|
|
28
33
|
// back to its customer and agent. `endModel` never rejects.
|
|
29
34
|
const reporting = new Map();
|
|
30
|
-
const
|
|
35
|
+
const defaultCall = JSON.stringify([stiggContext?.customerId, stiggContext?.agentId]);
|
|
36
|
+
const callOf = (params) => contexts.get(params) ?? defaultCall;
|
|
37
|
+
/** Waits for a call's stream reports in flight, so its next check sees their spend. */
|
|
38
|
+
const settle = (call) => Promise.all(reporting.get(call) ?? []);
|
|
39
|
+
// A tool's `execute` is handed only the call id, so each tool call a model step asks for is kept
|
|
40
|
+
// by its id - before the AI SDK can start the tool - and taken when it runs.
|
|
41
|
+
// Only for tools `tools()` wrapped: another's call - client-side, run by the provider - never runs
|
|
42
|
+
// here, and would only fill the map.
|
|
43
|
+
// ponytail: keyed by the provider's id alone. First-party providers make each id unique; a
|
|
44
|
+
// backend that reuses ids (`call_0`) across concurrent calls would let one call's entry replace
|
|
45
|
+
// another's - scope the key by call if one shows up. A call `experimental_repairToolCall` renames
|
|
46
|
+
// is not kept either, and is governed as one no step asked for.
|
|
47
|
+
const issued = new Map();
|
|
48
|
+
const wrapped = new Set();
|
|
49
|
+
const ask = (params, part) => {
|
|
50
|
+
if (part?.type !== 'tool-call' || typeof part.toolCallId !== 'string' || part.providerExecuted)
|
|
51
|
+
return;
|
|
52
|
+
if (typeof part.toolName !== 'string' || !wrapped.has(part.toolName))
|
|
53
|
+
return;
|
|
54
|
+
issued.delete(part.toolCallId);
|
|
55
|
+
issued.set(part.toolCallId, { context: contexts.get(params), call: callOf(params) });
|
|
56
|
+
if (issued.size > ISSUED_CALLS_LIMIT)
|
|
57
|
+
issued.delete(issued.keys().next().value);
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Said once per tool: a call no model step of this middleware asked for - approved after a pause
|
|
61
|
+
* (`needsApproval` runs it at the start of the next call, before any model step), answered on
|
|
62
|
+
* another instance, or asked by another middleware's model - and whose own context names no
|
|
63
|
+
* `stigg`, is governed by this middleware's own `stiggContext`, and reported for its customer.
|
|
64
|
+
*/
|
|
65
|
+
const warned = new Set();
|
|
66
|
+
const warnOnce = (name) => {
|
|
67
|
+
if (warned.has(name))
|
|
68
|
+
return;
|
|
69
|
+
warned.add(name);
|
|
70
|
+
// A logger that throws must not fail the tool it only reports on.
|
|
71
|
+
(0, hooks_1.safely)(() => stigg.config?.logger?.warn(`[stigg] tool "${name}" ran without a model step this middleware saw asking for it, and its ` +
|
|
72
|
+
"context names no `stigg`, so it is governed by the middleware's own stiggContext. An approved " +
|
|
73
|
+
`tool does this: name its customer in \`toolsContext: { ${name}: { stigg: { customerId } } }\`.`), () => undefined);
|
|
74
|
+
};
|
|
75
|
+
/** Checks a tool call as the step that asked for it. Refused: an error the model may read. */
|
|
76
|
+
const startTool = async (name, input, options) => {
|
|
77
|
+
const callId = typeof options?.toolCallId === 'string' ? options.toolCallId : undefined;
|
|
78
|
+
const issuedCall = callId === undefined ? undefined : issued.get(callId);
|
|
79
|
+
if (callId !== undefined)
|
|
80
|
+
issued.delete(callId);
|
|
81
|
+
// No model step asked for it here - an approved tool runs at the start of the next call - so
|
|
82
|
+
// the tool's own context may name who it is for: `toolsContext` (ai 7), `experimental_context` (ai 6).
|
|
83
|
+
const stated = issuedCall ? undefined : toolContextOf(options);
|
|
84
|
+
const asked = issuedCall ?? (stated ? { context: stated, call: stated } : undefined);
|
|
85
|
+
if (!asked)
|
|
86
|
+
warnOnce(name);
|
|
87
|
+
// A stream reports when it ends, and the AI SDK runs its tools before that: the tool's check
|
|
88
|
+
// waits, so it sees the spend of the step that asked for it.
|
|
89
|
+
await settle(asked?.call ?? defaultCall);
|
|
90
|
+
const runId = `${prefix}:tool:${++steps}`;
|
|
91
|
+
try {
|
|
92
|
+
await governance.startTool(runId, { name, callId, input }, { stiggContexts: asked?.context ? [asked.context] : [] });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const decision = (0, errors_1.isAccessDenied)(err);
|
|
96
|
+
if (!decision)
|
|
97
|
+
throw err;
|
|
98
|
+
// The AI SDK hands a failed tool's message to the model, so it says why, not who refused or
|
|
99
|
+
// what is left. The decision rides along as the cause, for `stigg.denial(error)`.
|
|
100
|
+
throw new Error(`${name} is not available: ${decision.reason ?? 'access denied'}.`, { cause: err });
|
|
101
|
+
}
|
|
102
|
+
return runId;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A tool whose `execute` is checked and reported. An `async function*` streams its results, and is
|
|
106
|
+
* reported by its last one, as the AI SDK takes it.
|
|
107
|
+
* ponytail: any other function returns a promise here, since the check is awaited first - so an
|
|
108
|
+
* async iterable it returns is run to its end and answered by its last value, without the
|
|
109
|
+
* preliminary results. Write it as `async function*` to stream them.
|
|
110
|
+
*/
|
|
111
|
+
const governTool = (name, tool) => {
|
|
112
|
+
const execute = tool.execute;
|
|
113
|
+
if (Object.prototype.toString.call(execute) === '[object AsyncGeneratorFunction]') {
|
|
114
|
+
return async function* (input, options) {
|
|
115
|
+
const runId = await startTool(name, input, options);
|
|
116
|
+
let output;
|
|
117
|
+
let finished = false;
|
|
118
|
+
try {
|
|
119
|
+
for await (const value of execute.call(tool, input, options)) {
|
|
120
|
+
output = value;
|
|
121
|
+
yield value;
|
|
122
|
+
}
|
|
123
|
+
finished = true;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (!finished)
|
|
127
|
+
governance.abandon(runId);
|
|
128
|
+
}
|
|
129
|
+
await governance.endTool(runId, output, name);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return async (input, options) => {
|
|
133
|
+
const runId = await startTool(name, input, options);
|
|
134
|
+
let output;
|
|
135
|
+
try {
|
|
136
|
+
output = await execute.call(tool, input, options);
|
|
137
|
+
if (isAsyncIterable(output)) {
|
|
138
|
+
let last;
|
|
139
|
+
for await (const value of output)
|
|
140
|
+
last = value;
|
|
141
|
+
output = last;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
governance.abandon(runId);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
// Awaited, and the AI SDK waits for every tool before the next step: that step's check sees it.
|
|
149
|
+
await governance.endTool(runId, output, name);
|
|
150
|
+
return output;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
31
153
|
/** Checks the step, as the agent and customer this call names. Refused: `AccessDeniedError`. */
|
|
32
154
|
const start = async (params, model, event) => {
|
|
33
155
|
// Once: a call's steps run one after another, so nothing of its own lands while this waits.
|
|
34
|
-
await
|
|
156
|
+
await settle(callOf(params));
|
|
35
157
|
const runId = `${prefix}:${++steps}`;
|
|
36
158
|
const context = contexts.get(params);
|
|
37
159
|
// Kept by the middleware until the step ends, so the model the reply names can still land on it.
|
|
@@ -42,6 +164,23 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
42
164
|
return {
|
|
43
165
|
specificationVersion: 'v3',
|
|
44
166
|
stigg: governance.stigg,
|
|
167
|
+
tools(tools) {
|
|
168
|
+
if (!toolConfig || Object.keys(toolConfig).length === 0) {
|
|
169
|
+
throw new Error("stiggVercelMiddleware's tools() needs a `tools` option: the paid tools by name, '*' for the rest.");
|
|
170
|
+
}
|
|
171
|
+
// Only a tool the config governs - by name, else through `'*'` - as the core looks it up. An
|
|
172
|
+
// exempt or unpriced one is left as it is: nothing would check it, so nothing should wait.
|
|
173
|
+
const governed = (name) => {
|
|
174
|
+
const entry = Object.hasOwn(toolConfig, name) ? toolConfig[name] : toolConfig[agent_middleware_1.ALL_TOOLS];
|
|
175
|
+
return entry !== undefined && entry !== false;
|
|
176
|
+
};
|
|
177
|
+
return Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
|
|
178
|
+
if (typeof tool?.execute !== 'function' || !governed(name))
|
|
179
|
+
return [name, tool];
|
|
180
|
+
wrapped.add(name);
|
|
181
|
+
return [name, { ...tool, execute: governTool(name, tool) }];
|
|
182
|
+
}));
|
|
183
|
+
},
|
|
45
184
|
async transformParams({ params }) {
|
|
46
185
|
const options = params?.providerOptions;
|
|
47
186
|
if (!options || typeof options !== 'object' || !('stigg' in options))
|
|
@@ -71,6 +210,8 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
71
210
|
const responseId = result?.response?.id;
|
|
72
211
|
const chars = (0, usage_1.contentChars)(result?.content);
|
|
73
212
|
served(trace, result?.response?.modelId);
|
|
213
|
+
for (const part of Array.isArray(result?.content) ? result.content : [])
|
|
214
|
+
ask(params, part);
|
|
74
215
|
await governance.endModel(runId, (0, usage_1.measurementOf)(result?.usage, responseId) ?? (chars ? (0, usage_1.estimated)(chars, usage_1.NO_USAGE, responseId) : undefined));
|
|
75
216
|
return result;
|
|
76
217
|
},
|
|
@@ -98,7 +239,7 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
98
239
|
if (pending.size === 0 && reporting.get(call) === pending)
|
|
99
240
|
reporting.delete(call);
|
|
100
241
|
});
|
|
101
|
-
});
|
|
242
|
+
}, (part) => ask(params, part));
|
|
102
243
|
return result;
|
|
103
244
|
},
|
|
104
245
|
};
|
|
@@ -180,7 +321,9 @@ function streamTally() {
|
|
|
180
321
|
* call's `abortSignal` - which is how the AI SDK stops a stream, without cancelling it.
|
|
181
322
|
* ponytail: a reader that stops with no `abortSignal` is not reported; nothing tells this layer.
|
|
182
323
|
*/
|
|
183
|
-
function observeStream(result, signal, done
|
|
324
|
+
function observeStream(result, signal, done,
|
|
325
|
+
/** Each part, before the AI SDK reads it - so a tool call is known before its tool can start. */
|
|
326
|
+
onPart) {
|
|
184
327
|
const source = result?.stream;
|
|
185
328
|
if (typeof source?.getReader !== 'function') {
|
|
186
329
|
done(undefined);
|
|
@@ -214,6 +357,11 @@ function observeStream(result, signal, done) {
|
|
|
214
357
|
return;
|
|
215
358
|
}
|
|
216
359
|
tally.record(next.value);
|
|
360
|
+
onPart?.(next.value);
|
|
361
|
+
// The AI SDK starts a step's tools on its `finish` part, before the stream closes - which, from
|
|
362
|
+
// an HTTP provider, is a later read. So the step reports here, and its tools' checks see it.
|
|
363
|
+
if (next.value?.type === 'finish')
|
|
364
|
+
report('ended');
|
|
217
365
|
controller.enqueue(next.value);
|
|
218
366
|
},
|
|
219
367
|
async cancel(reason) {
|
|
@@ -222,3 +370,17 @@ function observeStream(result, signal, done) {
|
|
|
222
370
|
},
|
|
223
371
|
});
|
|
224
372
|
}
|
|
373
|
+
const isAsyncIterable = (value) => typeof value?.[Symbol.asyncIterator] === 'function';
|
|
374
|
+
/** The `stigg` a tool's own context states - its `toolsContext` entry on ai 7, `experimental_context` on ai 6. */
|
|
375
|
+
function toolContextOf(options) {
|
|
376
|
+
const context = options?.context?.stigg ?? options?.experimental_context?.stigg;
|
|
377
|
+
if (context === undefined || context === null)
|
|
378
|
+
return undefined;
|
|
379
|
+
if (typeof context !== 'object' || Array.isArray(context)) {
|
|
380
|
+
throw new TypeError(`A tool's context \`stigg\` must be a stiggContext object - got ${typeof context}.`);
|
|
381
|
+
}
|
|
382
|
+
// On ai 6 the context is the whole app's, and `stigg` there may be the SDK instance itself: only
|
|
383
|
+
// a plain object is a stiggContext. Anything else names no one, and the SDK says so.
|
|
384
|
+
const proto = Object.getPrototypeOf(context);
|
|
385
|
+
return proto === Object.prototype || proto === null ? context : undefined;
|
|
386
|
+
}
|
|
@@ -141,28 +141,24 @@ export declare const OPENAI_METRICS: readonly ["prompt_tokens", "completion_toke
|
|
|
141
141
|
/** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
|
|
142
142
|
export declare const GATEWAY_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cost"];
|
|
143
143
|
/**
|
|
144
|
-
* A gateway that
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* this is the union of what its surfaces can report, which is what `metrics` is defined to be.
|
|
150
|
-
*
|
|
151
|
-
* No `cost`, and that is the defining fact rather than an oversight. Cloudflare states no price on
|
|
152
|
-
* any response: its figure lives in the gateway's own log, is explicitly an estimate, and is zero
|
|
153
|
-
* on a cache hit. So the recipe carries the log id as a dimension and lets spend be reconciled out
|
|
154
|
-
* of band, exactly as the gateway reference prescribes for a price that is only available
|
|
155
|
-
* asynchronously.
|
|
144
|
+
* A gateway that answers on the OpenAI routes and on Anthropic Messages, each in its own wire's
|
|
145
|
+
* vocabulary - so the union of the two, which is what `metrics` is defined to be.
|
|
146
|
+
*
|
|
147
|
+
* No `cost` on any of the gateways that use it, and that is the defining fact rather than an
|
|
148
|
+
* oversight: none of them states a price on the response, so spend is reconciled out of band.
|
|
156
149
|
*/
|
|
157
|
-
export declare const
|
|
150
|
+
export declare const OPENAI_ANTHROPIC_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
158
151
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* Whatever the upstream charged is invoiced by the upstream, out of band.
|
|
152
|
+
* Cloudflare AI Gateway's provider-native routes pass the provider's body straight through, unlike
|
|
153
|
+
* LiteLLM, which normalises every upstream to the OpenAI wire. Its price lives in the gateway's own
|
|
154
|
+
* log, is explicitly an estimate, and is zero on a cache hit - so the recipe carries the log id as
|
|
155
|
+
* a dimension instead.
|
|
164
156
|
*/
|
|
157
|
+
export declare const CLOUDFLARE_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
158
|
+
/** agentgateway is self-hosted and states no price; the upstream invoices out of band. */
|
|
165
159
|
export declare const AGENTGATEWAY_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
160
|
+
/** TrueFoundry tracks spend in its own dashboard; the provider accounts behind it are the caller's. */
|
|
161
|
+
export declare const TRUEFOUNDRY_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
166
162
|
/**
|
|
167
163
|
* Bedrock's five, under its own names and with no price among them.
|
|
168
164
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizedMetrics = exports.addMetrics = exports.METRIC_PATHS = exports.metricNamesMatchPaths = exports.COMPUTED_TOTALS = exports.metricName = exports.WORKERS_AI_METRICS = exports.WORKERS_AI_PATHS = exports.DATABRICKS_QUERY_METRIC_NAME_MAP = exports.DATABRICKS_METRICS = exports.DATABRICKS_RESPONSES_PATHS = exports.DATABRICKS_PATHS = exports.BEDROCK_METRICS = exports.BEDROCK_PATHS = exports.AGENTGATEWAY_METRICS = exports.CLOUDFLARE_METRICS = exports.GATEWAY_METRICS = exports.OPENAI_METRICS = exports.OLLAMA_METRICS = exports.OLLAMA_COMPONENT_METRICS = exports.TYPESAFE_METRICS = exports.TYPESAFE_COMPONENT_METRICS = exports.MISTRAL_METRICS = exports.MISTRAL_METRIC_NAME_MAP = exports.OPENAI_RESPONSES_METRICS = exports.OPENAI_CHAT_METRICS = exports.GEMINI_COMPONENT_METRICS = exports.GEMINI_METRICS = exports.ANTHROPIC_METRICS = exports.ANTHROPIC_COMPONENT_METRICS = exports.TOTAL_TOKENS = exports.addTokenUsage = void 0;
|
|
3
|
+
exports.normalizedMetrics = exports.addMetrics = exports.METRIC_PATHS = exports.metricNamesMatchPaths = exports.COMPUTED_TOTALS = exports.metricName = exports.WORKERS_AI_METRICS = exports.WORKERS_AI_PATHS = exports.DATABRICKS_QUERY_METRIC_NAME_MAP = exports.DATABRICKS_METRICS = exports.DATABRICKS_RESPONSES_PATHS = exports.DATABRICKS_PATHS = exports.BEDROCK_METRICS = exports.BEDROCK_PATHS = exports.TRUEFOUNDRY_METRICS = exports.AGENTGATEWAY_METRICS = exports.CLOUDFLARE_METRICS = exports.OPENAI_ANTHROPIC_METRICS = exports.GATEWAY_METRICS = exports.OPENAI_METRICS = exports.OLLAMA_METRICS = exports.OLLAMA_COMPONENT_METRICS = exports.TYPESAFE_METRICS = exports.TYPESAFE_COMPONENT_METRICS = exports.MISTRAL_METRICS = exports.MISTRAL_METRIC_NAME_MAP = exports.OPENAI_RESPONSES_METRICS = exports.OPENAI_CHAT_METRICS = exports.GEMINI_COMPONENT_METRICS = exports.GEMINI_METRICS = exports.ANTHROPIC_METRICS = exports.ANTHROPIC_COMPONENT_METRICS = exports.TOTAL_TOKENS = exports.addTokenUsage = void 0;
|
|
4
4
|
exports.fromFlatTokenUsage = fromFlatTokenUsage;
|
|
5
5
|
exports.fromOpenAIUsage = fromOpenAIUsage;
|
|
6
6
|
exports.fromResponsesUsage = fromResponsesUsage;
|
|
@@ -407,36 +407,28 @@ exports.OPENAI_METRICS = [
|
|
|
407
407
|
/** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
|
|
408
408
|
exports.GATEWAY_METRICS = [...exports.OPENAI_METRICS, 'cost'];
|
|
409
409
|
/**
|
|
410
|
-
* A gateway that
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* this is the union of what its surfaces can report, which is what `metrics` is defined to be.
|
|
416
|
-
*
|
|
417
|
-
* No `cost`, and that is the defining fact rather than an oversight. Cloudflare states no price on
|
|
418
|
-
* any response: its figure lives in the gateway's own log, is explicitly an estimate, and is zero
|
|
419
|
-
* on a cache hit. So the recipe carries the log id as a dimension and lets spend be reconciled out
|
|
420
|
-
* of band, exactly as the gateway reference prescribes for a price that is only available
|
|
421
|
-
* asynchronously.
|
|
410
|
+
* A gateway that answers on the OpenAI routes and on Anthropic Messages, each in its own wire's
|
|
411
|
+
* vocabulary - so the union of the two, which is what `metrics` is defined to be.
|
|
412
|
+
*
|
|
413
|
+
* No `cost` on any of the gateways that use it, and that is the defining fact rather than an
|
|
414
|
+
* oversight: none of them states a price on the response, so spend is reconciled out of band.
|
|
422
415
|
*/
|
|
423
|
-
exports.
|
|
416
|
+
exports.OPENAI_ANTHROPIC_METRICS = [
|
|
424
417
|
...exports.OPENAI_METRICS,
|
|
425
418
|
'cache_read_input_tokens',
|
|
426
419
|
'cache_creation_input_tokens',
|
|
427
420
|
];
|
|
428
421
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* Whatever the upstream charged is invoiced by the upstream, out of band.
|
|
422
|
+
* Cloudflare AI Gateway's provider-native routes pass the provider's body straight through, unlike
|
|
423
|
+
* LiteLLM, which normalises every upstream to the OpenAI wire. Its price lives in the gateway's own
|
|
424
|
+
* log, is explicitly an estimate, and is zero on a cache hit - so the recipe carries the log id as
|
|
425
|
+
* a dimension instead.
|
|
434
426
|
*/
|
|
435
|
-
exports.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
427
|
+
exports.CLOUDFLARE_METRICS = exports.OPENAI_ANTHROPIC_METRICS;
|
|
428
|
+
/** agentgateway is self-hosted and states no price; the upstream invoices out of band. */
|
|
429
|
+
exports.AGENTGATEWAY_METRICS = exports.OPENAI_ANTHROPIC_METRICS;
|
|
430
|
+
/** TrueFoundry tracks spend in its own dashboard; the provider accounts behind it are the caller's. */
|
|
431
|
+
exports.TRUEFOUNDRY_METRICS = exports.OPENAI_ANTHROPIC_METRICS;
|
|
440
432
|
/**
|
|
441
433
|
* Bedrock's five, under its own names and with no price among them.
|
|
442
434
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,9 @@ export type { CloudflareRecipeOptions } from './bindings/client/gateways/cloudfl
|
|
|
60
60
|
export { agentgatewayRecipe } from './bindings/client/gateways/agentgateway/agentgateway-recipe';
|
|
61
61
|
export type { AgentgatewayMetricName, AgentgatewayMetrics, } from './bindings/client/gateways/agentgateway/agentgateway-recipe';
|
|
62
62
|
export type { AgentgatewayRecipeOptions } from './bindings/client/gateways/agentgateway/types';
|
|
63
|
+
export { truefoundryRecipe } from './bindings/client/gateways/truefoundry/truefoundry-recipe';
|
|
64
|
+
export type { TrueFoundryMetricName, TrueFoundryMetrics, } from './bindings/client/gateways/truefoundry/truefoundry-recipe';
|
|
65
|
+
export type { TrueFoundryRecipeOptions } from './bindings/client/gateways/truefoundry/types';
|
|
63
66
|
export type { FetchLike, FetchOptions, GovernedFetch, GovernedRequestInit } from './bindings/fetch/types';
|
|
64
67
|
export { typesafeRecipe, estimateFromState } from './bindings/client/providers/typesafe/typesafe-recipe';
|
|
65
68
|
export type { TypeSafeMetric, TypeSafeMetrics } from './bindings/client/providers/typesafe/typesafe-recipe';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeUsage = exports.normalizedMetrics = exports.stiggVercelMiddleware = exports.weightedMetrics = exports.estimateFromState = exports.typesafeRecipe = exports.agentgatewayRecipe = exports.cloudflareRecipe = exports.bedrockRecipe = exports.databricksExternalRecipe = exports.databricksRecipe = exports.ollamaRecipe = exports.mistralRecipe = exports.groqRecipe = exports.litellmRecipe = exports.openrouterRecipe = exports.estimateFromMaxOutputTokens = exports.geminiChatRecipe = exports.geminiRecipe = exports.openAIRecipe = exports.estimateFromMaxTokens = exports.anthropicRecipe = exports.hasMethod = exports.createClientRegistry = exports.wrapClient = exports.toMeasurement = exports.entitlementRef = exports.entitlementKind = exports.entitlementId = exports.emptyTokenUsage = exports.vendorMetrics = exports.unitsPerTokens = exports.looksLikeGovernanceNotEnabled = exports.isGovernanceNotEnabled = exports.isAccessDenied = exports.isEngineError = exports.GovernanceNotEnabledError = exports.AccessDeniedError = exports.EngineError = exports.STIGG_TRACER_NAME = exports.noopLogger = exports.consoleLogger = exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = exports.remedyMessage = exports.describeDecision = exports.flattenRaw = exports.RAW_PREFIX = exports.StiggAgentSDK = void 0;
|
|
3
|
+
exports.normalizeUsage = exports.normalizedMetrics = exports.stiggVercelMiddleware = exports.weightedMetrics = exports.estimateFromState = exports.typesafeRecipe = exports.truefoundryRecipe = exports.agentgatewayRecipe = exports.cloudflareRecipe = exports.bedrockRecipe = exports.databricksExternalRecipe = exports.databricksRecipe = exports.ollamaRecipe = exports.mistralRecipe = exports.groqRecipe = exports.litellmRecipe = exports.openrouterRecipe = exports.estimateFromMaxOutputTokens = exports.geminiChatRecipe = exports.geminiRecipe = exports.openAIRecipe = exports.estimateFromMaxTokens = exports.anthropicRecipe = exports.hasMethod = exports.createClientRegistry = exports.wrapClient = exports.toMeasurement = exports.entitlementRef = exports.entitlementKind = exports.entitlementId = exports.emptyTokenUsage = exports.vendorMetrics = exports.unitsPerTokens = exports.looksLikeGovernanceNotEnabled = exports.isGovernanceNotEnabled = exports.isAccessDenied = exports.isEngineError = exports.GovernanceNotEnabledError = exports.AccessDeniedError = exports.EngineError = exports.STIGG_TRACER_NAME = exports.noopLogger = exports.consoleLogger = exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = exports.remedyMessage = exports.describeDecision = exports.flattenRaw = exports.RAW_PREFIX = exports.StiggAgentSDK = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Public surface.
|
|
6
6
|
*
|
|
@@ -126,6 +126,12 @@ Object.defineProperty(exports, "cloudflareRecipe", { enumerable: true, get: func
|
|
|
126
126
|
// and no `cost`, because the gateway states no price on any response.
|
|
127
127
|
var agentgateway_recipe_1 = require("./bindings/client/gateways/agentgateway/agentgateway-recipe");
|
|
128
128
|
Object.defineProperty(exports, "agentgatewayRecipe", { enumerable: true, get: function () { return agentgateway_recipe_1.agentgatewayRecipe; } });
|
|
129
|
+
// TrueFoundry's AI Gateway, hosted at `gateway.truefoundry.ai` or self-hosted. A default, ahead of both
|
|
130
|
+
// wires it serves, so its traffic reports as `truefoundry.*`. Self-hosted installs pass their base URL at
|
|
131
|
+
// `wrap(client, { recipe: truefoundryRecipe({ baseUrl }) })`. Tokens and the model that served
|
|
132
|
+
// (`x-tfy-resolved-model`), no `cost`.
|
|
133
|
+
var truefoundry_recipe_1 = require("./bindings/client/gateways/truefoundry/truefoundry-recipe");
|
|
134
|
+
Object.defineProperty(exports, "truefoundryRecipe", { enumerable: true, get: function () { return truefoundry_recipe_1.truefoundryRecipe; } });
|
|
129
135
|
var typesafe_recipe_1 = require("./bindings/client/providers/typesafe/typesafe-recipe");
|
|
130
136
|
Object.defineProperty(exports, "typesafeRecipe", { enumerable: true, get: function () { return typesafe_recipe_1.typesafeRecipe; } });
|
|
131
137
|
Object.defineProperty(exports, "estimateFromState", { enumerable: true, get: function () { return typesafe_recipe_1.estimateFromState; } });
|
package/dist/stigg-agent-sdk.js
CHANGED
|
@@ -28,6 +28,7 @@ const litellm_recipe_1 = require("./bindings/client/gateways/litellm/litellm-rec
|
|
|
28
28
|
const databricks_recipe_1 = require("./bindings/client/gateways/databricks/databricks-recipe");
|
|
29
29
|
const cloudflare_recipe_1 = require("./bindings/client/gateways/cloudflare/cloudflare-recipe");
|
|
30
30
|
const agentgateway_recipe_1 = require("./bindings/client/gateways/agentgateway/agentgateway-recipe");
|
|
31
|
+
const truefoundry_recipe_1 = require("./bindings/client/gateways/truefoundry/truefoundry-recipe");
|
|
31
32
|
const groq_recipe_1 = require("./bindings/client/providers/groq/groq-recipe");
|
|
32
33
|
const mistral_recipe_1 = require("./bindings/client/providers/mistral/mistral-recipe");
|
|
33
34
|
const ollama_recipe_1 = require("./bindings/client/providers/ollama/ollama-recipe");
|
|
@@ -71,6 +72,9 @@ exports.DEFAULT_RECIPES = [
|
|
|
71
72
|
// Anthropic Messages wire too, and only the base URL tells its `Anthropic` client from a direct
|
|
72
73
|
// one. Without a configured base URL it matches on a hostname label - see the recipe.
|
|
73
74
|
(0, agentgateway_recipe_1.agentgatewayRecipe)(),
|
|
75
|
+
// Ahead of `anthropicRecipe` for the same reason again: TrueFoundry answers on both wires, and
|
|
76
|
+
// only its hostname (or a configured base URL) tells its clients from direct ones.
|
|
77
|
+
(0, truefoundry_recipe_1.truefoundryRecipe)(),
|
|
74
78
|
anthropic_recipe_1.anthropicRecipe,
|
|
75
79
|
(0, openrouter_recipe_1.openrouterRecipe)(),
|
|
76
80
|
(0, litellm_recipe_1.litellmRecipe)(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigg/agent-sdk-js",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Stigg agent SDK (JS) - run-lifecycle governance for agentic stacks, with examples per layer",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"example-gateway-openrouter": "tsx examples/gateway/openrouter.ts",
|
|
65
65
|
"example-gateway-litellm": "tsx examples/gateway/litellm.ts",
|
|
66
66
|
"example-gateway-agentgateway": "tsx examples/gateway/agentgateway.ts",
|
|
67
|
+
"example-gateway-truefoundry": "tsx examples/gateway/truefoundry.ts",
|
|
67
68
|
"example-gateway-cloudflare": "tsx examples/gateway/cloudflare.ts",
|
|
68
69
|
"example-gateway-bedrock": "tsx examples/gateway/bedrock.ts",
|
|
69
70
|
"example-gateway-databricks": "tsx examples/gateway/databricks.ts"
|