@stigg/agent-sdk-js 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -3
- package/dist/bindings/client/core/types.d.ts +9 -0
- package/dist/bindings/client/core/wrap-client.js +5 -2
- package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.d.ts +10 -0
- package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.js +254 -0
- package/dist/bindings/client/gateways/cloudflare/types.d.ts +35 -0
- package/dist/bindings/client/gateways/cloudflare/types.js +2 -0
- package/dist/bindings/client/gateways/databricks/databricks-recipe.d.ts +32 -0
- package/dist/bindings/client/gateways/databricks/databricks-recipe.js +324 -0
- package/dist/bindings/client/gateways/databricks/types.d.ts +30 -0
- package/dist/bindings/client/gateways/databricks/types.js +2 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +6 -19
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +3 -14
- package/dist/bindings/client/providers/groq/groq-recipe.js +3 -20
- package/dist/bindings/shared/base-url.d.ts +44 -0
- package/dist/bindings/shared/base-url.js +90 -0
- package/dist/bindings/shared/token-usage.d.ts +106 -1
- package/dist/bindings/shared/token-usage.js +162 -1
- package/dist/bindings/shared/types.d.ts +41 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +5 -1
- package/dist/stigg-agent-sdk.d.ts +1 -1
- package/dist/stigg-agent-sdk.js +13 -0
- package/dist/types.d.ts +35 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -256,6 +256,9 @@ that vendor's API would have guessed — so these are what your meters filter on
|
|
|
256
256
|
| Mistral | `mistral.chat.complete`, `mistral.chat.stream`, `mistral.chat.parse`, `mistral.chat.parseStream`, `mistral.fim.complete`, `mistral.fim.stream`, `mistral.agents.complete`, `mistral.agents.stream` | `prompt_tokens`, `completion_tokens`, `total_tokens` — the **wire's** names, not the camelCase its SDK renames them to |
|
|
257
257
|
| Ollama | `ollama.chat`, `ollama.generate` | `prompt_eval_count`, `eval_count`, `total_tokens` — self-hosted, so these measure tokens you served yourself rather than tokens anyone billed you for |
|
|
258
258
|
| Bedrock | `bedrock.send` | `inputTokens`, `outputTokens`, `cacheReadInputTokens`, `cacheWriteInputTokens`, `totalTokens` — and no `cost`, because Bedrock states none |
|
|
259
|
+
| Databricks | `databricks.chat.completions.create`, `databricks.responses.create`, `databricks.query` | `prompt_tokens`, `completion_tokens`, `total_tokens`, `reasoning_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens` — and no `cost`, because Databricks states none on any response. `databricks.query` is the first-party client and reports the first three only; it camelCases the wire and is renamed back, so both clients report one vocabulary |
|
|
260
|
+
| Databricks — external models | `databricks-external.chat.completions.create`, `databricks-external.responses.create`, `databricks-external.query` | the same six. A separate vendor name because a different party is invoiced, and it is **opt-in**: the two routes are indistinguishable from the client, so naming it asserts something the SDK cannot check |
|
|
261
|
+
| Cloudflare | `cloudflare.chat.completions.create`, `cloudflare.responses.create`, `cloudflare.messages.create`, `cloudflare.ai.run` | the OpenAI set plus `cache_read_input_tokens`, `cache_creation_input_tokens` — the union across every wire it serves, and a call reports only the names its own route sent. No `cost` and no neuron count: neither Workers AI nor AI Gateway states a price on any response, so `cf_aig_log_id` rides as a dimension instead. A gateway **cache hit meters nothing**, because nobody was billed for it |
|
|
259
262
|
| TypeSafe | `typesafe.systemOne` | `input_tokens`, `output_tokens`, `total_tokens` |
|
|
260
263
|
| Gemini | `gemini.models.generateContent`, `gemini.models.generateContentStream` | `promptTokenCount`, `candidatesTokenCount`, `cachedContentTokenCount`, `thoughtsTokenCount`, `toolUsePromptTokenCount`, `totalTokenCount` |
|
|
261
264
|
| Gemini — a chat | `gemini.chats.sendMessage`, `gemini.chats.sendMessageStream` | the same five |
|
|
@@ -372,6 +375,7 @@ both.**
|
|
|
372
375
|
| OpenAI Responses | `input_tokens` | **Yes**, includes `cached_tokens` |
|
|
373
376
|
| LangChain | `input_tokens` | **Yes**, includes both cache keys |
|
|
374
377
|
| Gemini | `promptTokenCount` | **Yes**, includes `cachedContentTokenCount` |
|
|
378
|
+
| Databricks | `prompt_tokens` | **Unverified** — Databricks documents neither direction. It spells the pair with Anthropic's names (`cache_read_input_tokens`), whose counters sit beside the input count, so this SDK does not subtract them. Its stated `total_tokens` is read rather than computed, so the total is right either way — price the total, or verify against your own endpoint before pricing the breakdown |
|
|
375
379
|
|
|
376
380
|
Gemini overlaps in both directions at once, so it is worth stating separately: pricing
|
|
377
381
|
`promptTokenCount` **and** `cachedContentTokenCount` double-bills the cached tokens, while
|
|
@@ -420,6 +424,7 @@ are recipes over the same wrapper. One you **operate** is the opposite binding a
|
|
|
420
424
|
| Client | `stigg.wrap(client, { stiggContext })` | gates the recipe's methods on a copy, vendor matched by shape | Yes — when the context names an entitlement |
|
|
421
425
|
| Client — a gateway you **call** | `wrap(client, { recipe: openrouterRecipe() })` | same wrapper; the recipe bills `usage.cost` and attributes `response.model` | Yes, same as any client |
|
|
422
426
|
| Client — a gateway whose cost is in a **header** | `wrap(client, { recipe: litellmRecipe() })` | same wrapper; the recipe bills `x-litellm-response-cost` and attributes `response.model` | Yes, same as any client |
|
|
427
|
+
| Client — a gateway that states **no** price | `wrap(client, { recipe: databricksRecipe() })` | same wrapper; no `cost` metric at all, and the model that answered is attributed instead | Yes, same as any client |
|
|
423
428
|
| Gateway you **operate** (`bindings/proxy`) | `client.stigg.headers()` → `stigg.ingestFromHeaders(...)` | attribution travels as `x-stigg-*` headers; `vendor` names the shape it observed | **No — ingest only** |
|
|
424
429
|
|
|
425
430
|
A client run that names no `entitlement` lands in the same column as the gateway — metering
|
|
@@ -449,6 +454,9 @@ which recipe matches, and that decides what you can meter:
|
|
|
449
454
|
| `https://openrouter.ai/api/v1` | `openrouterRecipe` | `openrouter.*` (see below) | tokens, `cost`, model that served |
|
|
450
455
|
| the LiteLLM proxy URL you configured | `litellmRecipe` | `litellm.*` | tokens, `cost`, model that served |
|
|
451
456
|
| `https://api.groq.com/openai/v1` | `groqRecipe` | `groq.*` | tokens only |
|
|
457
|
+
| a Databricks workspace `/serving-endpoints` | `databricksRecipe` | `databricks.*` | tokens, model that answered |
|
|
458
|
+
| `https://api.cloudflare.com/client/v4/accounts/<id>/ai/v1` | `cloudflareRecipe` | `cloudflare.*` | tokens only |
|
|
459
|
+
| `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/<provider>` | `cloudflareRecipe` | `cloudflare.*` | tokens, model that served, log id |
|
|
452
460
|
| anything else OpenAI-compatible | `openAIRecipe` | `openai.*` | tokens only |
|
|
453
461
|
|
|
454
462
|
Each of those covers **both OpenAI surfaces**, and a surface is its own event: Chat Completions
|
|
@@ -521,9 +529,106 @@ name to match.
|
|
|
521
529
|
> `for await` bypasses the meter, because the stream is your object and cannot be swapped for a
|
|
522
530
|
> metered copy. Call the methods, and iterate the stream.
|
|
523
531
|
|
|
524
|
-
> **
|
|
525
|
-
>
|
|
526
|
-
>
|
|
532
|
+
> **OpenRouter, both ways round.** OpenRouter's own `@openrouter/sdk` package is a different shape
|
|
533
|
+
> — its methods are `chat.send` and `responses.send`, and its counters come back camelCased — but
|
|
534
|
+
> `openrouterRecipe` declares those surfaces too, so `wrap` governs that client as well as an
|
|
535
|
+
> `OpenAI` one pointed at `https://openrouter.ai/api/v1`. Four event names rather than two
|
|
536
|
+
> (`openrouter.chat.send` beside `openrouter.chat.completions.create`), because a meter filtering
|
|
537
|
+
> on one must not catch the other. Both report the **wire's** counter names: the SDK's camelCase is
|
|
538
|
+
> a rename of the same block, and one account should not read as two vocabularies.
|
|
539
|
+
|
|
540
|
+
> **Databricks, as two recipes over one wire.** Databricks model serving is OpenAI-compatible, so
|
|
541
|
+
> an `OpenAI` client pointed at `https://<workspace>/serving-endpoints` is all the application
|
|
542
|
+
> holds. Two things make it worth a recipe rather than the `openAIRecipe` row above.
|
|
543
|
+
>
|
|
544
|
+
> The first is that **the request does not say what model is being used.** On the classic route the
|
|
545
|
+
> `model` argument is a serving *endpoint* name the customer chose — `prod-chat` is legal, and it
|
|
546
|
+
> may front DBRX, a Llama that Databricks hosts, or somebody's OpenAI account. The response's
|
|
547
|
+
> `model` field is what recovers that, and `databricksRecipe` puts it on the ingest as a dimension.
|
|
548
|
+
> Metered as `openai.*` the whole workspace is one undifferentiated stream.
|
|
549
|
+
>
|
|
550
|
+
> The second is **who gets invoiced**, and this one is a decision you have to make rather than one
|
|
551
|
+
> the SDK can make for you. Foundation Model API traffic (`databricks-claude-*`, `databricks-llama-*`,
|
|
552
|
+
> DBRX) is billed to your Databricks account in DBUs. An endpoint configured as an *external model*
|
|
553
|
+
> carries your own OpenAI or Anthropic key, and that provider invoices your account with them —
|
|
554
|
+
> Databricks only estimates the figure, into `system.ai_gateway.external_model_spend`, and says it
|
|
555
|
+
> is not liable for the difference. Same host, same path, same request, same response: **nothing on
|
|
556
|
+
> the client side distinguishes them.** So the split is an assertion, not a detection —
|
|
557
|
+
>
|
|
558
|
+
> ```ts
|
|
559
|
+
> stigg.wrap(client, { recipe: databricksExternalRecipe() });
|
|
560
|
+
> ```
|
|
561
|
+
>
|
|
562
|
+
> — and `databricksExternalRecipe` is deliberately not registered by default, because an assertion
|
|
563
|
+
> nobody made is not a default. Assert wrongly and nothing throws: DBU traffic lands on the meter
|
|
564
|
+
> you keep for upstream spend, or vice versa. If one workspace serves both kinds, wrap one client
|
|
565
|
+
> per kind.
|
|
566
|
+
>
|
|
567
|
+
> **Neither recipe reports a `cost`**, on either route — Databricks states no price on any
|
|
568
|
+
> response, in the body or in a header. Reconcile spend against its system tables out of band.
|
|
569
|
+
>
|
|
570
|
+
> Recognised by workspace host **and** serving path: the three public families
|
|
571
|
+
> (`cloud.databricks.com`, `azuredatabricks.net`, `gcp.databricks.com`) crossed with
|
|
572
|
+
> `/serving-endpoints` or `/ai-gateway/mlflow/v1`. The host alone is every Databricks API there is,
|
|
573
|
+
> most of which are not inference. A PrivateLink or custom-DNS workspace matches neither half —
|
|
574
|
+
> give the recipe its base URL: `databricksRecipe({ baseUrl })`.
|
|
575
|
+
>
|
|
576
|
+
> **Both clients, one recipe.** `@databricks/sdk-modelservingquery` is Databricks' own JS client,
|
|
577
|
+
> and its whole surface is one method — `client.query({ name, messages })`, reported as
|
|
578
|
+
> `databricks.query`. It is a second *surface* rather than a second vendor, because one Databricks
|
|
579
|
+
> account is billed whichever client the application imported; splitting it would split one bill
|
|
580
|
+
> across two meters. It is recognised automatically, so `wrap(client)` governs it with no recipe
|
|
581
|
+
> named.
|
|
582
|
+
>
|
|
583
|
+
> Three things are worth knowing before you choose it. It **camelCases the wire** — its generated
|
|
584
|
+
> parser turns `prompt_tokens` into `promptTokens` — so this SDK renames the counters back, and
|
|
585
|
+
> both clients report `prompt_tokens` for the same workspace. It reports **three counters, not
|
|
586
|
+
> six**: its usage type has no cache pair and no `reasoning_tokens`, which are absent rather than
|
|
587
|
+
> zero. And it **cannot stream**, and its request carries no `tools`, `response_format` or
|
|
588
|
+
> `stream_options` — so for real work the OpenAI-compatible route above is still the richer one,
|
|
589
|
+
> and it is what the Databricks docs show.
|
|
590
|
+
>
|
|
591
|
+
> One method serves chat, completions, embeddings and custom-model predictions, so the event name
|
|
592
|
+
> cannot say which ran. The response's own `object` field rides as a `response_object` dimension
|
|
593
|
+
> so a meter can tell them apart.
|
|
594
|
+
|
|
595
|
+
> **Cloudflare Workers AI is a provider, not a gateway.** Cloudflare runs these models on its own
|
|
596
|
+
> GPUs and invoices them to your Cloudflare account, which is the test this package applies. The
|
|
597
|
+
> `@cf/<author>/<model>` ids look like a reseller's catalogue and are not one — `<author>` is who
|
|
598
|
+
> *made* the weights, not who bills for running them, and `@cf/openai/gpt-oss-120b` is OpenAI's
|
|
599
|
+
> open-weights release at Cloudflare's own per-token rate rather than OpenAI API pricing. (`@hf/…`
|
|
600
|
+
> ids are Workers AI too, and about an eighth of the catalogue, so anything keying on the `@cf/`
|
|
601
|
+
> prefix alone is wrong.)
|
|
602
|
+
>
|
|
603
|
+
> Two clients reach it and both report under `cloudflare`, because one account is billed
|
|
604
|
+
> whichever is held: an `OpenAI` client pointed at
|
|
605
|
+
> `https://api.cloudflare.com/client/v4/accounts/<id>/ai/v1`, and the `cloudflare` package's own
|
|
606
|
+
> client, whose method is `ai.run(model, inputs)`. Two event names, so a meter can tell the wires
|
|
607
|
+
> apart.
|
|
608
|
+
>
|
|
609
|
+
> **It publishes no cost and no neuron count.** Cloudflare bills Workers AI in Neurons and puts a
|
|
610
|
+
> neuron figure on no response at all — not the binding's, not either REST route's — so converting
|
|
611
|
+
> tokens to Neurons needs Cloudflare's per-model rate table, which this SDK does not hold and will
|
|
612
|
+
> not guess at. Tokens are what the response states and tokens are what it reports.
|
|
613
|
+
>
|
|
614
|
+
> ⚠️ **A streamed `ai.run` is gated but not metered.** Asked for a stream, that method hands back
|
|
615
|
+
> the raw SSE byte stream rather than parsed chunks, so there is no object with a `usage` block to
|
|
616
|
+
> read — and the recipe attaches no reader rather than one that would find nothing and report a
|
|
617
|
+
> zero. The call is still checked before it leaves; the measurement afterwards is what is missing.
|
|
618
|
+
> Stream through the OpenAI-compatible route instead, where usage arrives on the final chunk, or
|
|
619
|
+
> call `ai.run` buffered.
|
|
620
|
+
>
|
|
621
|
+
> `ai.run` also carries **no model dimension**, because the model is that method's first argument
|
|
622
|
+
> and an extractor only ever sees the result. Nothing is lost that matters — Workers AI does not
|
|
623
|
+
> route, so what was asked for is what ran — but a meter that wants it can have it from
|
|
624
|
+
> `stiggContext: { dimensions: { model: '@cf/meta/llama-3.1-8b-instruct' } }`.
|
|
625
|
+
>
|
|
626
|
+
> One wrinkle about that hostname: `…/ai/v1` also serves **third-party** models (a bare
|
|
627
|
+
> `author/model` id like `openai/gpt-4.1`) routed through AI Gateway and paid out of Cloudflare
|
|
628
|
+
> credits under Unified Billing. Those land on this recipe, which is defensible rather than a
|
|
629
|
+
> mistake — Cloudflare is the payer either way — and the model is on the response, so a meter that
|
|
630
|
+
> wants them apart can split on it. AI Gateway proper lives on `gateway.ai.cloudflare.com` and has
|
|
631
|
+
> its own recipe.
|
|
527
632
|
|
|
528
633
|
Client recipes are data — method paths and usage extractors — so the factory wires the built-in
|
|
529
634
|
ones with no vendor package loaded. Nothing in `src/index.ts` pulls a vendor package in at
|
|
@@ -980,6 +1085,13 @@ there (or exporting it in your shell) overrides the fixture without editing `dem
|
|
|
980
1085
|
| `AGENT_SDK_BEDROCK_REGION` | `us-east-1` (falls back to `AWS_REGION`) | Region the Bedrock model is called in. There is no Bedrock key variable: AWS authenticates through its own credential chain, and the example asks that chain rather than looking for one |
|
|
981
1086
|
| `AGENT_SDK_BEDROCK_MODEL` | `anthropic.claude-sonnet-4-5-20250929-v1:0` | Bedrock model id. Names one model exactly — Bedrock does not route, so there is no fallback list |
|
|
982
1087
|
| `AGENT_SDK_LITELLM_API_KEY` | — (falls back to `LITELLM_API_KEY`) | Layer 2. Blank ⇒ the offline mock |
|
|
1088
|
+
| `AGENT_SDK_CLOUDFLARE_API_KEY` | none | Cloudflare API token with Workers AI read access (falls back to `CLOUDFLARE_API_TOKEN`) |
|
|
1089
|
+
| `AGENT_SDK_CLOUDFLARE_ACCOUNT_ID` | none | Required by every `ai.run` call — it is part of the URL path, not the body |
|
|
1090
|
+
| `AGENT_SDK_CLOUDFLARE_MODEL` | `@cf/meta/llama-3.1-8b-instruct` | A Workers AI model id. `@hf/…` ids are Workers AI too, so a matcher keyed on `@cf/` alone misses about an eighth of the catalogue |
|
|
1091
|
+
| `AGENT_SDK_DATABRICKS_API_KEY` | — (falls back to `DATABRICKS_TOKEN`) | A workspace token — a PAT, or an OAuth M2M token. Blank ⇒ the offline mock. Read by both Databricks examples |
|
|
1092
|
+
| `AGENT_SDK_DATABRICKS_BASE_URL` | none | Your workspace — customer-specific, so there is no default that could be right. `/serving-endpoints` is appended if you leave it off, and a PrivateLink workspace works because the recipe is handed this URL rather than recognising the host |
|
|
1093
|
+
| `AGENT_SDK_DATABRICKS_MODEL` | `databricks-claude-sonnet-4-6` | A serving **endpoint** name, not a model id — which is why the served model has to be read off the response. Read by both Databricks examples |
|
|
1094
|
+
| `AGENT_SDK_DATABRICKS_EXTERNAL` | unset | `1` asserts the endpoint carries your own upstream key, so that provider invoices it rather than Databricks. An assertion the SDK cannot check — it switches both Databricks examples to `databricks-external.*` |
|
|
983
1095
|
| `AGENT_SDK_LITELLM_BASE_URL` | `http://localhost:4000` | Your own proxy — self-hosted, so there is no public default |
|
|
984
1096
|
| `AGENT_SDK_LITELLM_MODEL` | `gpt-5` | A model **group** from the proxy's config, not a provider slug |
|
|
985
1097
|
| `STIGG_API_KEY` | — | **Server** key (`server-…`). A publishable `client-…` key fails every call with `IdentityForbidden`. Blank ⇒ the built-in Stigg mock |
|
|
@@ -1051,11 +1163,14 @@ Stigg account.
|
|
|
1051
1163
|
| `example-client-ollama` | [`client/ollama.ts`](./examples/client/ollama.ts) | A self-hosted engine that bills nobody: a flat client, counters flat on the response, and capacity rather than spend |
|
|
1052
1164
|
| `example-client-openrouter` | [`client/openrouter.ts`](./examples/client/openrouter.ts) | `openrouterRecipe()` billing the credits actually charged instead of a token count |
|
|
1053
1165
|
| `example-client-litellm` | [`client/litellm.ts`](./examples/client/litellm.ts) | The same script against a self-hosted proxy. Streams, so it meters tokens — see the gateway example for why |
|
|
1166
|
+
| `example-client-databricks` | [`client/databricks.ts`](./examples/client/databricks.ts) | An `OpenAI` client pointed at a workspace `/serving-endpoints`. The request names an **endpoint**, not a model, so the served model on the way back is the thing to watch. `AGENT_SDK_DATABRICKS_EXTERNAL=1` asserts the external-model route |
|
|
1054
1167
|
| `example-client-typesafe` | [`client/typesafe.ts`](./examples/client/typesafe.ts) | A non-chat client: typed questions, batched or one per call. **Needs a real TypeSafe key** |
|
|
1055
1168
|
| `example-two-agents` | [`client/two-agents.ts`](./examples/client/two-agents.ts) | Two agents, one team: a sibling denied by its own cap, then the survivor denied by the team above it |
|
|
1056
1169
|
| `example-gateway-openrouter`| [`gateway/openrouter.ts`](./examples/gateway/openrouter.ts) | The two gateway topologies: calling one gates, operating one only ingests. `example-gateway` runs this one |
|
|
1057
1170
|
| `example-gateway-litellm` | [`gateway/litellm.ts`](./examples/gateway/litellm.ts) | The same two topologies when the cost is in a header: real spend, then tokens, from one request |
|
|
1058
1171
|
| `example-gateway-bedrock` | [`gateway/bedrock.ts`](./examples/gateway/bedrock.ts) | A gateway that states no price: tokens are all there is to meter, and nothing proxies it |
|
|
1172
|
+
| `example-gateway-databricks` | [`gateway/databricks.ts`](./examples/gateway/databricks.ts) | Databricks' **own** client, `@databricks/sdk-modelservingquery` — one method, `client.query(…)`, governed by the same recipe as the row above and reported as `databricks.query`. Shows the camelCase counters renamed back to the wire's, and a budget draining until it refuses |
|
|
1173
|
+
| `example-gateway-cloudflare` | [`gateway/cloudflare.ts`](./examples/gateway/cloudflare.ts) | Cloudflare's own client (`ai.run`). The one example that does not stream — a streamed `ai.run` hands back raw SSE bytes, so there is no usage block to meter |
|
|
1059
1174
|
| `example-setup` | [`shared/setup-governance.ts`](./examples/shared/setup-governance.ts) | Seeds a customer, a pool and per-entity budgets. Idempotent |
|
|
1060
1175
|
|
|
1061
1176
|
`example-client` is an alias for the Anthropic one. Prefix any of them with `AGENT_SDK_OFFLINE=1`
|
|
@@ -32,6 +32,15 @@ export type MethodRecipe = {
|
|
|
32
32
|
* shape offers beyond a promise.
|
|
33
33
|
*/
|
|
34
34
|
path: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Which argument carries `stiggContext`, when it is not the first.
|
|
37
|
+
*
|
|
38
|
+
* Most vendor methods take one options object, so the context rides in argument 0 and this is
|
|
39
|
+
* left alone. A method whose first argument is not that object needs to say so - Cloudflare's
|
|
40
|
+
* `ai.run(model, inputs)` names the model first - or a caller's per-call context is forwarded
|
|
41
|
+
* to the vendor as an unknown field and the gate silently uses the wrap-time one instead.
|
|
42
|
+
*/
|
|
43
|
+
contextArg?: number;
|
|
35
44
|
/**
|
|
36
45
|
* True when this call is an internal leg of another wrapped method and must not be gated
|
|
37
46
|
* twice. Vendors mark these differently, so only the recipe can tell.
|
|
@@ -105,7 +105,10 @@ function patch(copy, source, session, base, method, eventName) {
|
|
|
105
105
|
owner[key] = function gated(...args) {
|
|
106
106
|
// Whatever the vendor's own signature takes. Naming the first two and forwarding only those
|
|
107
107
|
// would quietly drop the rest of a call the recipe never claimed to reshape.
|
|
108
|
-
|
|
108
|
+
// Argument 0 unless the method says otherwise: a vendor whose first argument is not its
|
|
109
|
+
// options object would otherwise have a caller's context forwarded to it verbatim.
|
|
110
|
+
const contextArg = method.contextArg ?? 0;
|
|
111
|
+
let params = args[contextArg];
|
|
109
112
|
let context = defaults;
|
|
110
113
|
// The context rides inside the vendor's own parameter object, so it has to come back out
|
|
111
114
|
// before anything - the recipe's own hooks included - ever sees those parameters. Both the
|
|
@@ -113,7 +116,7 @@ function patch(copy, source, session, base, method, eventName) {
|
|
|
113
116
|
if (params?.stiggContext !== undefined) {
|
|
114
117
|
const { stiggContext, ...rest } = params;
|
|
115
118
|
params = rest;
|
|
116
|
-
args =
|
|
119
|
+
args = args.map((arg, i) => (i === contextArg ? rest : arg));
|
|
117
120
|
context = (0, context_1.overrideContext)(defaults, stiggContext);
|
|
118
121
|
}
|
|
119
122
|
const callOptions = args[1];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CLOUDFLARE_METRICS } from '../../../shared/token-usage';
|
|
2
|
+
import type { CloudflareRecipeOptions } from './types';
|
|
3
|
+
import type { ClientRecipe, MetricsFor, MetricsOf } from '../../core/types';
|
|
4
|
+
type CloudflareMetric = (typeof CLOUDFLARE_METRICS)[number];
|
|
5
|
+
export declare function cloudflareRecipe(options?: CloudflareRecipeOptions): ClientRecipe<CloudflareMetric>;
|
|
6
|
+
/** One of the metric names Cloudflare can report, across every wire it serves. */
|
|
7
|
+
export type CloudflareMetricName = MetricsOf<ReturnType<typeof cloudflareRecipe>>;
|
|
8
|
+
/** What a mapper reads off a Cloudflare call: those names, each possibly absent. */
|
|
9
|
+
export type CloudflareMetrics = MetricsFor<ReturnType<typeof cloudflareRecipe>>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloudflareRecipe = cloudflareRecipe;
|
|
4
|
+
/**
|
|
5
|
+
* The Cloudflare recipe - one vendor over two products.
|
|
6
|
+
*
|
|
7
|
+
* **Workers AI** runs open-weight models on Cloudflare's GPUs, reached as `client.ai.run(...)` or
|
|
8
|
+
* as an `OpenAI` client at `api.cloudflare.com`. **AI Gateway** fronts somebody else's provider at
|
|
9
|
+
* `gateway.ai.cloudflare.com/v1/<account>/<gateway>/<provider>` and forwards that provider's body
|
|
10
|
+
* back untouched. One recipe, because one payer: Cloudflare invoices either way, and the vendor is
|
|
11
|
+
* whoever bills. Two vendor names would split one account across two meters.
|
|
12
|
+
*
|
|
13
|
+
* Filed under gateways although Workers AI alone looks provider-shaped - the same shape
|
|
14
|
+
* `groqRecipe` is argued from. That is deliberate: the category follows how a platform is sold,
|
|
15
|
+
* not how it is built. See the positioning section of the `govern-new-platform` skill.
|
|
16
|
+
*
|
|
17
|
+
* Two things follow from the gateway forwarding bodies verbatim, and both surprise people:
|
|
18
|
+
*
|
|
19
|
+
* - **Two counter vocabularies under one vendor.** `/openai` reports `prompt_tokens` and
|
|
20
|
+
* `/anthropic` reports `input_tokens`, so a meter watching one misses the other. Nothing is
|
|
21
|
+
* renamed - a counter has to reconcile against its own upstream's dashboard - so price both,
|
|
22
|
+
* or use `normalizedMetrics`. `metrics` is the union, which is what `metrics` is defined to be.
|
|
23
|
+
* - **A cache hit is a free call carrying a full `usage` block.** See {@link isCacheHit}: metered
|
|
24
|
+
* generically, every hit bills as real usage, and caching is this gateway's headline feature.
|
|
25
|
+
*
|
|
26
|
+
* Neither product states a price anywhere, so no `cost` metric is declared and `cf-aig-log-id`
|
|
27
|
+
* rides as a dimension for out-of-band reconciliation.
|
|
28
|
+
*/
|
|
29
|
+
const meter_stream_1 = require("../../../shared/meter-stream");
|
|
30
|
+
const token_usage_1 = require("../../../shared/token-usage");
|
|
31
|
+
const base_url_1 = require("../../../shared/base-url");
|
|
32
|
+
const registry_1 = require("../../core/registry");
|
|
33
|
+
const openai_recipe_1 = require("../../providers/openai/openai-recipe");
|
|
34
|
+
const anthropic_recipe_1 = require("../../providers/anthropic/anthropic-recipe");
|
|
35
|
+
const types_1 = require("../../../../types");
|
|
36
|
+
/** Workers AI, both of its routes. */
|
|
37
|
+
const CLOUDFLARE_API_HOST = 'api.cloudflare.com';
|
|
38
|
+
/** AI Gateway's public endpoint. A custom domain needs `options.baseUrl` instead. */
|
|
39
|
+
const GATEWAY_HOST = 'gateway.ai.cloudflare.com';
|
|
40
|
+
/** `HIT` means the upstream was never called, so nobody was billed for this. */
|
|
41
|
+
const CACHE_STATUS_HEADER = 'cf-aig-cache-status';
|
|
42
|
+
/** The gateway's own log record, where its (estimated) cost figure can be read afterwards. */
|
|
43
|
+
const LOG_ID_HEADER = 'cf-aig-log-id';
|
|
44
|
+
/** Auto Router's choice, which can differ from the model the request named. */
|
|
45
|
+
const ROUTED_MODEL_HEADER = 'cf-aig-routed-model';
|
|
46
|
+
/** Which fallback step served: `0` is the first provider, anything higher means one failed over. */
|
|
47
|
+
const STEP_HEADER = 'cf-aig-step';
|
|
48
|
+
/** The gateway's own request id, stable across its retries - unlike a local step counter. */
|
|
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
|
+
/**
|
|
55
|
+
* Whether the gateway served this from its cache.
|
|
56
|
+
*
|
|
57
|
+
* Readable on a streamed response as well as a buffered one, unlike a header-borne *cost*: whether
|
|
58
|
+
* the cache will answer is known before any token is produced, so this header is on the wire in
|
|
59
|
+
* time either way. That is the difference between this and `x-litellm-response-cost`, and it is why
|
|
60
|
+
* the streaming branch below checks it rather than giving up.
|
|
61
|
+
*
|
|
62
|
+
* Absent on the Workers AI routes, which have no cache in front of them - so this reads `false`
|
|
63
|
+
* there and the surfaces behave as ordinary metered calls.
|
|
64
|
+
*/
|
|
65
|
+
const isCacheHit = (response) => headerOf(response, CACHE_STATUS_HEADER)?.toUpperCase() === 'HIT';
|
|
66
|
+
/** What only the gateway knows, folded onto whatever the upstream's own reader produced. */
|
|
67
|
+
function withGatewayFacts(measured, result, response, modelDimension) {
|
|
68
|
+
const dimensions = { ...measured.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;
|
|
74
|
+
// 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);
|
|
76
|
+
if (logId)
|
|
77
|
+
dimensions.cf_aig_log_id = logId;
|
|
78
|
+
// Only when a fallback actually happened. Step `0` is the ordinary case and tagging every event
|
|
79
|
+
// with it would add a dimension to all of them for no information.
|
|
80
|
+
const step = headerOf(response, STEP_HEADER);
|
|
81
|
+
if (step !== undefined && step !== '0')
|
|
82
|
+
dimensions.cf_aig_step = step;
|
|
83
|
+
return {
|
|
84
|
+
...measured,
|
|
85
|
+
dimensions: Object.keys(dimensions).length > 0 ? dimensions : undefined,
|
|
86
|
+
idempotencyKey: headerOf(response, REQUEST_ID_HEADER) ?? measured.idempotencyKey ?? result?.id,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** The Workers AI reader: the three counters it states, under its own (OpenAI's) names. */
|
|
90
|
+
function measure(usage, model, id, modelDimension, keys = token_usage_1.WORKERS_AI_PATHS) {
|
|
91
|
+
if (!usage)
|
|
92
|
+
return undefined;
|
|
93
|
+
return {
|
|
94
|
+
// Anything absent is absent: a model that reported no counters meters nothing rather than a
|
|
95
|
+
// row of zeroes.
|
|
96
|
+
metrics: (0, token_usage_1.pickMetrics)(usage, keys),
|
|
97
|
+
// The OpenAI reader, because these are the OpenAI counters - and it finds no nested cache
|
|
98
|
+
// detail here, which is correct, because Workers AI sends none.
|
|
99
|
+
usage: (0, token_usage_1.fromOpenAIUsage)(usage),
|
|
100
|
+
raw: usage,
|
|
101
|
+
dimensions: model ? { [modelDimension]: model } : undefined,
|
|
102
|
+
idempotencyKey: id,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function cloudflareRecipe(options = {}) {
|
|
106
|
+
const estimate = options.estimate ?? (() => 1);
|
|
107
|
+
const modelDimension = options.modelDimension ?? 'model';
|
|
108
|
+
/**
|
|
109
|
+
* One upstream surface, wrapped.
|
|
110
|
+
*
|
|
111
|
+
* The upstream recipes' own extractors are reused rather than reimplemented, which is the point:
|
|
112
|
+
* AI Gateway forwards the provider's body untouched, so the provider's reader is by definition
|
|
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.
|
|
116
|
+
*/
|
|
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
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Chat Completions, which both products answer on - Workers AI's OpenAI-compatible route and the
|
|
162
|
+
* gateway's `/openai` one.
|
|
163
|
+
*
|
|
164
|
+
* Its own reader rather than `openAIRecipe`'s, for one reason: the model has to be tracked
|
|
165
|
+
* across the chunks of a *streamed* turn. The generic reader does not carry it, and off the
|
|
166
|
+
* gateway there is no `cf-aig-routed-model` header to recover it from - so reusing that reader
|
|
167
|
+
* would silently drop model attribution on every streamed Workers AI call. The key list is the
|
|
168
|
+
* full OpenAI one so a gateway-forwarded `cached_tokens` is not dropped either; Workers AI sends
|
|
169
|
+
* none of those, and absent stays absent.
|
|
170
|
+
*/
|
|
171
|
+
const chatSurface = {
|
|
172
|
+
path: ['chat', 'completions', 'create'],
|
|
173
|
+
extractUsage: (result) => measure(result?.usage, result?.model, result?.id, modelDimension, token_usage_1.METRIC_PATHS.openaiChat),
|
|
174
|
+
attachStream: (stream, ingest) => {
|
|
175
|
+
let usage;
|
|
176
|
+
let model;
|
|
177
|
+
let id;
|
|
178
|
+
// The OpenAI recipe's own chunk reader, not a copy of it. AI Gateway forwards whatever the
|
|
179
|
+
// upstream sent, and an upstream can hide its usage somewhere other than the chunk root -
|
|
180
|
+
// Groq puts it on `x_groq` and sends no top-level `usage` at all - so reading `chunk.usage`
|
|
181
|
+
// here would meter a streamed Groq call through this gateway at nothing. Borrowing the
|
|
182
|
+
// reader keeps every upstream that recipe learns about working through this one too.
|
|
183
|
+
const readStreamedUsage = (0, openai_recipe_1.openAISurface)(['chat', 'completions', 'create']).streamed;
|
|
184
|
+
(0, meter_stream_1.meterStream)(stream, (chunk) => {
|
|
185
|
+
// Stated once and whole on the final chunk rather than as deltas, so the last wins.
|
|
186
|
+
const streamedUsage = readStreamedUsage(chunk);
|
|
187
|
+
if (streamedUsage)
|
|
188
|
+
usage = streamedUsage;
|
|
189
|
+
if (chunk?.model)
|
|
190
|
+
model = chunk.model;
|
|
191
|
+
if (chunk?.id)
|
|
192
|
+
id = chunk.id;
|
|
193
|
+
}, () => {
|
|
194
|
+
const measurement = measure(usage, model, id, modelDimension, token_usage_1.METRIC_PATHS.openaiChat);
|
|
195
|
+
if (measurement)
|
|
196
|
+
ingest(measurement);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* The `cloudflare` package's own method.
|
|
202
|
+
*
|
|
203
|
+
* Two limits live here rather than in the file header, because they are facts about this method.
|
|
204
|
+
*
|
|
205
|
+
* It resolves to a **bare promise with no response object**, so no `cf-aig-*` header is reachable
|
|
206
|
+
* on this route - cache status included. A Workers AI call made this way through a gateway is
|
|
207
|
+
* metered as an ordinary call even when the gateway served it free. The `OpenAI`-client route
|
|
208
|
+
* has the headers and does not have this gap.
|
|
209
|
+
*
|
|
210
|
+
* And no `attachStream`, deliberately. A streamed `ai.run` resolves to raw SSE bytes
|
|
211
|
+
* rather than parsed chunks, and a reader attached to those would collect nothing and report it
|
|
212
|
+
* as a measurement of zero, which is the one failure shape nothing downstream can notice. Gated
|
|
213
|
+
* and unmetered is the honest outcome, and it is documented rather than discovered.
|
|
214
|
+
*/
|
|
215
|
+
const aiRun = {
|
|
216
|
+
path: ['ai', 'run'],
|
|
217
|
+
// `ai.run(model, inputs)` - the model is argument 0, so the context rides in argument 1. Left
|
|
218
|
+
// at the default this surface would forward a caller's `stiggContext` to Cloudflare as an
|
|
219
|
+
// unknown input and gate on the wrap-time context instead, which is the quiet kind of wrong.
|
|
220
|
+
contextArg: 1,
|
|
221
|
+
// No model: it was the first argument, and an extractor is handed only the result. No id
|
|
222
|
+
// either - this surface returns neither, so idempotency falls back to the caller's.
|
|
223
|
+
extractUsage: (result) => measure(result?.usage, undefined, undefined, modelDimension),
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
vendor: 'cloudflare',
|
|
227
|
+
metrics: token_usage_1.CLOUDFLARE_METRICS,
|
|
228
|
+
// Every wire here spells its total `total_tokens` - Anthropic's is computed from its disjoint
|
|
229
|
+
// counters by the reader this reuses, OpenAI's and Workers AI's are stated.
|
|
230
|
+
totalTokensMetric: token_usage_1.TOTAL_TOKENS,
|
|
231
|
+
// The host decides, because shape cannot: an `OpenAI` client and an `Anthropic` client pointed
|
|
232
|
+
// at either product look exactly like direct clients. `ai.run` is checked as well as the two
|
|
233
|
+
// shapes because the `cloudflare` package's client has no `chat.completions`.
|
|
234
|
+
matches: (client) => {
|
|
235
|
+
const baseUrl = (0, base_url_1.baseUrlOf)(client);
|
|
236
|
+
if (baseUrl === undefined)
|
|
237
|
+
return false;
|
|
238
|
+
const onCloudflare = options.baseUrl
|
|
239
|
+
? (0, base_url_1.withinBaseUrl)(options.baseUrl, baseUrl)
|
|
240
|
+
: (0, base_url_1.matchesHost)(baseUrl, CLOUDFLARE_API_HOST) || (0, base_url_1.matchesHost)(baseUrl, GATEWAY_HOST);
|
|
241
|
+
if (!onCloudflare)
|
|
242
|
+
return false;
|
|
243
|
+
return (0, registry_1.hasMethod)(client, ['ai', 'run']) || openai_recipe_1.openAIRecipe.matches(client) || anthropic_recipe_1.anthropicRecipe.matches(client);
|
|
244
|
+
},
|
|
245
|
+
// Every wire it serves. A surface left undeclared here is not metered by something else - it is
|
|
246
|
+
// ungated, because this recipe is matched ahead of both of the ones that would have claimed it.
|
|
247
|
+
methods: [
|
|
248
|
+
surfaceOf(chatSurface),
|
|
249
|
+
...openai_recipe_1.openAIRecipe.methods.filter((m) => m.path.join('.') !== 'chat.completions.create').map(surfaceOf),
|
|
250
|
+
...anthropic_recipe_1.anthropicRecipe.methods.map(surfaceOf),
|
|
251
|
+
surfaceOf(aiRun),
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type CloudflareRecipeOptions = {
|
|
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, because there is nothing fractional to
|
|
7
|
+
* denominate: Cloudflare publishes no price on any response, on either route, so a run here
|
|
8
|
+
* bills token counters, which arrive whole.
|
|
9
|
+
*
|
|
10
|
+
* Worth knowing that a cache hit passes the gate and then meters nothing. That is the right way
|
|
11
|
+
* round - whether a call will hit the gateway's cache cannot be known before it is made, and a
|
|
12
|
+
* free call should not be billed afterwards.
|
|
13
|
+
*/
|
|
14
|
+
estimate?: (params: any) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Dimension the model that actually served is attributed to.
|
|
17
|
+
*
|
|
18
|
+
* Which model that is depends on the route. Workers AI does not route, so what was asked for is
|
|
19
|
+
* what ran - and `ai.run` takes the model as its first *argument* and echoes nothing back, so a
|
|
20
|
+
* turn made that way carries no model dimension at all. Through AI Gateway it really can differ:
|
|
21
|
+
* Auto Router picks from a candidate pool and names its choice in `cf-aig-routed-model`, and a
|
|
22
|
+
* fallback chain can serve from a different provider entirely.
|
|
23
|
+
*/
|
|
24
|
+
modelDimension?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The Cloudflare endpoint, when it is not one of the two public ones.
|
|
27
|
+
*
|
|
28
|
+
* Workers AI is served from `api.cloudflare.com` and AI Gateway from
|
|
29
|
+
* `gateway.ai.cloudflare.com/v1/<account>/<gateway>/<provider>`; the matcher recognises both
|
|
30
|
+
* without being told. The case that needs this is a **custom domain**: a gateway served from
|
|
31
|
+
* `ai.example.com` has the account and gateway ids stripped out of the path, so nothing in the
|
|
32
|
+
* URL identifies Cloudflare at all.
|
|
33
|
+
*/
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DATABRICKS_METRICS } from '../../../shared/token-usage';
|
|
2
|
+
import type { DatabricksRecipeOptions } from './types';
|
|
3
|
+
import type { ClientRecipe, MetricsFor, MetricsOf } from '../../core/types';
|
|
4
|
+
type DatabricksMetric = (typeof DATABRICKS_METRICS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Databricks-hosted inference - Foundation Model APIs, billed to your Databricks account in DBUs.
|
|
7
|
+
*
|
|
8
|
+
* Registered by default and matched on the workspace URL, because reading a richer response is not
|
|
9
|
+
* a pricing decision anyone needs to opt into. What it adds over `openAIRecipe` is a vendor name
|
|
10
|
+
* and the model that answered, which on this platform is the only thing that says what an endpoint
|
|
11
|
+
* name meant.
|
|
12
|
+
*/
|
|
13
|
+
export declare const databricksRecipe: (options?: DatabricksRecipeOptions) => ClientRecipe<DatabricksMetric>;
|
|
14
|
+
/**
|
|
15
|
+
* The external-model route - Databricks proxies, your upstream provider invoices.
|
|
16
|
+
*
|
|
17
|
+
* Not registered by default: which route an endpoint serves is server-side configuration, so this
|
|
18
|
+
* is the caller asserting something about their own workspace rather than the SDK detecting it.
|
|
19
|
+
* Name it at `wrap(client, { recipe: databricksExternalRecipe() })`, and see the file header for
|
|
20
|
+
* what a wrong assertion costs.
|
|
21
|
+
*
|
|
22
|
+
* `databricks-external` rather than something further from `databricks`: the two sort together in
|
|
23
|
+
* a meter list, which is what you want when you are reading one account's traffic split across
|
|
24
|
+
* them, and `external` is Databricks' own word - an endpoint of this kind is configured under an
|
|
25
|
+
* `external_model` block.
|
|
26
|
+
*/
|
|
27
|
+
export declare const databricksExternalRecipe: (options?: DatabricksRecipeOptions) => ClientRecipe<DatabricksMetric>;
|
|
28
|
+
/** One of the metric names Databricks reports. */
|
|
29
|
+
export type DatabricksMetricName = MetricsOf<ReturnType<typeof databricksRecipe>>;
|
|
30
|
+
/** What a mapper reads off a Databricks call: those names, each possibly absent. */
|
|
31
|
+
export type DatabricksMetrics = MetricsFor<ReturnType<typeof databricksRecipe>>;
|
|
32
|
+
export {};
|