zuplo 7.9.18 → 7.9.19

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.
@@ -0,0 +1,175 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema",
3
+ "$id": "https://cdn.zuplo.com/policies/runtime/schemas/ai-gateway-load-balancing-inbound.json",
4
+ "type": "object",
5
+ "title": "Model Load Balancing",
6
+ "isDeprecated": false,
7
+ "isPaidAddOn": false,
8
+ "isEnterprise": false,
9
+ "isInternal": false,
10
+ "isBeta": false,
11
+ "isHidden": false,
12
+ "requiresAI": true,
13
+ "policyType": "ai-gateway-load-balancing",
14
+ "products": ["ai-gateway"],
15
+ "description": "Spreads AI requests across a list of models. The policy cycles through the list, or keeps requests that share a key, such as a user or a conversation, on the same model. Most requests also get a retry model from the list. A request moves there when the chosen model stays rate limited. Chat completions and embeddings requests also move there when the model returns a server error, can't be reached, or times out.\n\nThe policy picks the model for every request it handles and ignores the request's own `model`, so the list also limits which models clients can reach. An earlier policy's model selection is never overwritten.",
16
+ "deprecatedMessage": "",
17
+ "required": ["handler"],
18
+ "properties": {
19
+ "handler": {
20
+ "type": "object",
21
+ "default": {},
22
+ "required": ["export", "module", "options"],
23
+ "properties": {
24
+ "export": {
25
+ "const": "AIGatewayLoadBalancingInboundPolicy",
26
+ "description": "The name of the exported type"
27
+ },
28
+ "module": {
29
+ "const": "$import(@zuplo/runtime)",
30
+ "description": "The module containing the policy"
31
+ },
32
+ "options": {
33
+ "type": "object",
34
+ "title": "AIGatewayLoadBalancingInboundPolicyOptions",
35
+ "description": "Options for spreading AI Gateway requests across a list of models.",
36
+ "additionalProperties": false,
37
+ "required": ["models"],
38
+ "examples": [
39
+ {
40
+ "strategy": "round-robin",
41
+ "models": {
42
+ "completions": [
43
+ "azure-eastus/gpt-5",
44
+ "azure-westus/gpt-5",
45
+ "openai/gpt-5"
46
+ ]
47
+ }
48
+ }
49
+ ],
50
+ "properties": {
51
+ "strategy": {
52
+ "type": "string",
53
+ "title": "Strategy",
54
+ "description": "How the policy picks a model for each request. `round-robin` cycles through the list in order. `sticky` sends every request with the same key to the same model.",
55
+ "enum": ["round-robin", "sticky"],
56
+ "default": "round-robin"
57
+ },
58
+ "stickyBy": {
59
+ "type": "string",
60
+ "title": "Sticky By",
61
+ "description": "The key that `sticky` uses. `user` uses the authenticated caller, `request.user.sub`. `conversation` uses the conversation's system prompt and its first user message with text. `expression` uses the value that `expression` selects. Required when `strategy` is `sticky`, and not allowed with `round-robin`.",
62
+ "enum": ["user", "conversation", "expression"]
63
+ },
64
+ "expression": {
65
+ "type": "string",
66
+ "title": "Expression",
67
+ "description": "The value to key on, written like a [Metering budget expression](https://zuplo.com/docs/policies/ai-gateway-metering-inbound#budget-expressions), such as `request.headers.get(\"x-end-user-id\")`. Required when `stickyBy` is `expression`, and not allowed otherwise.",
68
+ "minLength": 1
69
+ },
70
+ "models": {
71
+ "type": "object",
72
+ "title": "Models",
73
+ "description": "The models to spread requests across, by request type. Set at least one list.",
74
+ "additionalProperties": false,
75
+ "minProperties": 1,
76
+ "properties": {
77
+ "completions": {
78
+ "type": "array",
79
+ "title": "Completions Models",
80
+ "description": "Models for chat completions, Responses, Messages, and System One requests, as `providerName/model`. From 1 through 32 models, each listed once. Provider names aren't case-sensitive, so `openai/gpt-5` and `OpenAI/gpt-5` are the same model. Write each model ID exactly as your provider spells it.",
81
+ "minItems": 1,
82
+ "maxItems": 32,
83
+ "uniqueItems": true,
84
+ "items": {
85
+ "type": "string",
86
+ "title": "Provider and Model",
87
+ "description": "A model reference in providerName/model form, split on the first slash.",
88
+ "pattern": "^[^/\\s]+/.+$"
89
+ }
90
+ },
91
+ "embeddings": {
92
+ "type": "array",
93
+ "title": "Embeddings Models",
94
+ "description": "Models for embeddings requests, as `providerName/model`. From 1 through 32 models, each listed once. Provider names aren't case-sensitive. Write each model ID exactly as your provider spells it. Every entry must be the same embedding model, such as one model on several accounts.",
95
+ "minItems": 1,
96
+ "maxItems": 32,
97
+ "uniqueItems": true,
98
+ "items": {
99
+ "type": "string",
100
+ "title": "Provider and Model",
101
+ "description": "A model reference in providerName/model form, split on the first slash.",
102
+ "pattern": "^[^/\\s]+/.+$"
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "fallbackTimeoutSeconds": {
108
+ "type": "integer",
109
+ "title": "Fallback Timeout Seconds",
110
+ "description": "How long to wait for a model to start responding before retrying the request on another model. Applies to chat completions and embeddings. The timeout ends when the response starts, so it doesn't cut off a long streaming response. A response that doesn't stream starts only when it's complete, so the timeout covers the whole response. From 1 through 300.",
111
+ "minimum": 1,
112
+ "maximum": 300,
113
+ "default": 60
114
+ }
115
+ },
116
+ "if": {
117
+ "required": ["strategy"],
118
+ "properties": {
119
+ "strategy": {
120
+ "const": "sticky"
121
+ }
122
+ }
123
+ },
124
+ "then": {
125
+ "required": ["stickyBy"],
126
+ "if": {
127
+ "required": ["stickyBy"],
128
+ "properties": {
129
+ "stickyBy": {
130
+ "const": "expression"
131
+ }
132
+ }
133
+ },
134
+ "then": {
135
+ "required": ["expression"]
136
+ },
137
+ "else": {
138
+ "not": {
139
+ "required": ["expression"]
140
+ }
141
+ }
142
+ },
143
+ "else": {
144
+ "not": {
145
+ "anyOf": [
146
+ {
147
+ "required": ["stickyBy"]
148
+ },
149
+ {
150
+ "required": ["expression"]
151
+ }
152
+ ]
153
+ }
154
+ }
155
+ }
156
+ },
157
+ "examples": [
158
+ {
159
+ "export": "AIGatewayLoadBalancingInboundPolicy",
160
+ "module": "$import(@zuplo/runtime)",
161
+ "options": {
162
+ "strategy": "round-robin",
163
+ "models": {
164
+ "completions": [
165
+ "azure-eastus/gpt-5",
166
+ "azure-westus/gpt-5",
167
+ "openai/gpt-5"
168
+ ]
169
+ }
170
+ }
171
+ }
172
+ ]
173
+ }
174
+ }
175
+ }
@@ -23,11 +23,11 @@ agentic, document_qa, other) and the built-in classification prompt.
23
23
 
24
24
  ## Choose a classifier
25
25
 
26
- | Classifier | Set | Use it when |
27
- | ----------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `classifier.app` | `appId`, `model` | You want the classifier to use one of your providers, through an AI Gateway app in this project. The call stays in-process. Set `adapter: "messages"` for Claude models, or `adapter: "systemOne"` for Jev and other System One models. |
29
- | `classifier.chatCompletions` (deprecated) | `apiKey`, `model` | You need to call an OpenAI-compatible endpoint directly with its own key, without a second AI Gateway app. |
30
- | `classifier.jev` (deprecated) | `apiKey` | You need to call Jev, from TypeSafe, directly with its own key. TypeSafe and OpenRouter both serve it. |
26
+ | Classifier | Set | Use it when |
27
+ | ----------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `classifier.app` | `appId`, `model` | You want the classifier to use one of your providers, through an AI Gateway app in this project. The call stays in-process. Set `adapter: "messages"` for Claude models, or `adapter: "system-one"` for Jev and other System One models. |
29
+ | `classifier.chatCompletions` (deprecated) | `apiKey`, `model` | You need to call an OpenAI-compatible endpoint directly with its own key, without a second AI Gateway app. |
30
+ | `classifier.jev` (deprecated) | `apiKey` | You need to call Jev, from TypeSafe, directly with its own key. TypeSafe and OpenRouter both serve it. |
31
31
 
32
32
  The three are mutually exclusive. Setting more than one is rejected with a
33
33
  `ConfigurationError` rather than guessed at, because routing on a classifier you
@@ -50,9 +50,9 @@ budgets, and analytics, and new classifier features are added only to
50
50
  ```
51
51
 
52
52
  - `appId` — the AI Gateway app that runs the classifier.
53
- - `adapter` — optional: `chatCompletions` (default), `messages`, or `systemOne`.
54
- Selects `/v1/chat/completions`, `/v1/messages`, or `/v1/systemone` on that
55
- app.
53
+ - `adapter` — optional: `chat-completions` (default), `messages`, or
54
+ `system-one`. Selects `/v1/chat/completions`, `/v1/messages`, or
55
+ `/v1/systemone` on that app.
56
56
  - `model` — the `providerName/model` that app routes the classifier request to.
57
57
  - `apiKey` — optional. See
58
58
  [Authenticating the classifier app](#authenticating-the-classifier-app).
@@ -116,7 +116,7 @@ For Jev registered with your gateway, use the same app path with System One:
116
116
  "classifier": {
117
117
  "app": {
118
118
  "appId": "$env(CLASSIFIER_APP_ID)",
119
- "adapter": "systemOne",
119
+ "adapter": "system-one",
120
120
  "model": "jev/jev-latest"
121
121
  }
122
122
  }
@@ -209,7 +209,7 @@ routing applies only when Jev is sure of both. Jev writes no reasons, so
209
209
 
210
210
  Jev takes no system prompt, so setting `classifierPrompt` with `classifier.jev`
211
211
  is rejected with a `ConfigurationError`. The same restriction applies to
212
- `classifier.app.adapter: "systemOne"`.
212
+ `classifier.app.adapter: "system-one"`.
213
213
 
214
214
  `chatCompletions` and `jev` call a service outside the gateway with the key you
215
215
  configure, and send it the prompt. When that service answers with an error, the
@@ -218,11 +218,11 @@ which can echo the request back, prompt included.
218
218
 
219
219
  To migrate a direct section, register the provider with the gateway, create a
220
220
  classifier app for it, and replace the section with `classifier.app`. Use
221
- `adapter: "systemOne"` to replace `classifier.jev`, and `adapter: "messages"`
221
+ `adapter: "system-one"` to replace `classifier.jev`, and `adapter: "messages"`
222
222
  for a Claude model. Existing `classifier.app` configurations continue to default
223
- to Chat Completions when `adapter` is omitted. `systemOne` uses the existing Jev
224
- request and response contract; it does not add support for arbitrary decision
225
- APIs.
223
+ to Chat Completions when `adapter` is omitted. `system-one` uses the existing
224
+ Jev request and response contract; it does not add support for arbitrary
225
+ decision APIs.
226
226
 
227
227
  ## Authenticating the classifier app
228
228
 
@@ -361,7 +361,7 @@ classifier calls a strict JSON-schema chat completion, so it can only return one
361
361
  of your configured ids. The `description` values are only shown to the
362
362
  classifier if your prompt includes `{{intents}}` (see below).
363
363
 
364
- With `classifier.jev` or `classifier.app.adapter: "systemOne"`, the `id` values
364
+ With `classifier.jev` or `classifier.app.adapter: "system-one"`, the `id` values
365
365
  are the options of Jev's intent question, and every `description` is shown to
366
366
  Jev with its option. There is no prompt, so `{{intents}}` doesn't apply.
367
367
 
@@ -372,9 +372,9 @@ so it's never eligible for routing.
372
372
  ### Custom classifier prompt
373
373
 
374
374
  `classifierPrompt` replaces the built-in classifier prompt for `classifier.app`
375
- (its default `chatCompletions` adapter, or `messages`, which sends it as
375
+ (its default `chat-completions` adapter, or `messages`, which sends it as
376
376
  `system`) and `classifier.chatCompletions`.
377
- `classifier.app.adapter: "systemOne"` and `classifier.jev` take no prompt, so
377
+ `classifier.app.adapter: "system-one"` and `classifier.jev` take no prompt, so
378
378
  combining `classifierPrompt` with either is a configuration error. It accepts
379
379
  either a string or an array of lines:
380
380
 
@@ -457,7 +457,7 @@ interface AIGatewaySmartRouterResult {
457
457
  }
458
458
  ```
459
459
 
460
- With `classifier.jev` or `classifier.app.adapter: "systemOne"`,
460
+ With `classifier.jev` or `classifier.app.adapter: "system-one"`,
461
461
  `classifierModel` is the versioned model that answered, in the host's naming:
462
462
  `jev-1.13.0` on TypeSafe, or `typesafe/jev-1.13-20260917` on OpenRouter. `usage`
463
463
  reports the input and output tokens that the Jev API returns as `inputTokens`
@@ -550,7 +550,7 @@ like smart routing quietly doing nothing:
550
550
  - More than one section in `classifier` (`app`, `chatCompletions`, and `jev`).
551
551
  - A deprecated `classifierAppID`, `classifierModel`, or `classifierAppApiKey`
552
552
  next to `classifier`.
553
- - `classifierPrompt` with `classifier.app.adapter: "systemOne"` or
553
+ - `classifierPrompt` with `classifier.app.adapter: "system-one"` or
554
554
  `classifier.jev`.
555
555
  - A `classifier.app.appId` that names this app.
556
556
  - A classifier app that runs this policy or Akamai AI Firewall.
@@ -63,7 +63,7 @@
63
63
  "classifier": {
64
64
  "app": {
65
65
  "appId": "$env(CLASSIFIER_APP_ID)",
66
- "adapter": "systemOne",
66
+ "adapter": "system-one",
67
67
  "model": "jev/jev-latest"
68
68
  }
69
69
  },
@@ -92,7 +92,7 @@
92
92
  "classifier": {
93
93
  "app": {
94
94
  "appId": "$env(CLASSIFIER_APP_ID)",
95
- "adapter": "chatCompletions",
95
+ "adapter": "chat-completions",
96
96
  "model": "openai/gpt-4o-mini"
97
97
  }
98
98
  },
@@ -141,10 +141,10 @@
141
141
  },
142
142
  "adapter": {
143
143
  "type": "string",
144
- "enum": ["chatCompletions", "messages", "systemOne"],
145
- "default": "chatCompletions",
144
+ "enum": ["chat-completions", "messages", "system-one"],
145
+ "default": "chat-completions",
146
146
  "title": "Classifier App Adapter",
147
- "description": "API contract exposed by the classifier app. chatCompletions calls /v1/chat/completions; messages calls /v1/messages for Claude models that support structured outputs (`output_config.format`), such as `claude-haiku-4-5`, on a provider whose Messages endpoint accepts them (not Amazon Bedrock Mantle); systemOne calls /v1/systemone for models such as Jev. The app owns provider credentials, pricing, metering and analytics."
147
+ "description": "API contract exposed by the classifier app. `chat-completions` calls /v1/chat/completions; `messages` calls /v1/messages for Claude models that support structured outputs (`output_config.format`), such as `claude-haiku-4-5`, on a provider whose Messages endpoint accepts them (not Amazon Bedrock Mantle); `system-one` calls /v1/systemone for models such as Jev. The app owns provider credentials, pricing, metering and analytics."
148
148
  }
149
149
  }
150
150
  },
@@ -181,7 +181,7 @@
181
181
  "jev": {
182
182
  "type": "object",
183
183
  "title": "Jev (TypeSafe)",
184
- "description": "Deprecated: use `classifier.app` with `adapter` `systemOne` instead. Classify with Jev, TypeSafe's classification model, by calling a System One API directly: TypeSafe's by default, or another host such as OpenRouter through `baseUrl`. Jev answers typed questions instead of generating text, so it takes no `classifierPrompt`.",
184
+ "description": "Deprecated: use `classifier.app` with `adapter` `system-one` instead. Classify with Jev, TypeSafe's classification model, by calling a System One API directly: TypeSafe's by default, or another host such as OpenRouter through `baseUrl`. Jev answers typed questions instead of generating text, so it takes no `classifierPrompt`.",
185
185
  "additionalProperties": false,
186
186
  "required": ["apiKey"],
187
187
  "properties": {
@@ -279,7 +279,7 @@
279
279
  },
280
280
  "classifierPrompt": {
281
281
  "title": "Classifier Prompt",
282
- "description": "Prompt for classifier.app (its default chatCompletions adapter or the messages adapter, where it is sent as the system prompt) and classifier.chatCompletions. Omit to use the built-in prompt. classifier.app.adapter systemOne and classifier.jev take no prompt; combining them with classifierPrompt is a configuration error.",
282
+ "description": "Prompt for classifier.app (its default `chat-completions` adapter or the `messages` adapter, where it is sent as the system prompt) and classifier.chatCompletions. Omit to use the built-in prompt. classifier.app.adapter `system-one` and classifier.jev take no prompt; combining them with classifierPrompt is a configuration error.",
283
283
  "oneOf": [
284
284
  {
285
285
  "type": "string",
@@ -360,7 +360,7 @@
360
360
  "required": ["adapter"],
361
361
  "properties": {
362
362
  "adapter": {
363
- "const": "systemOne"
363
+ "const": "system-one"
364
364
  }
365
365
  }
366
366
  }
@@ -396,7 +396,7 @@
396
396
  "classifier": {
397
397
  "app": {
398
398
  "appId": "$env(CLASSIFIER_APP_ID)",
399
- "adapter": "systemOne",
399
+ "adapter": "system-one",
400
400
  "model": "jev/jev-latest"
401
401
  }
402
402
  },
@@ -433,7 +433,7 @@
433
433
  "classifier": {
434
434
  "app": {
435
435
  "appId": "$env(CLASSIFIER_APP_ID)",
436
- "adapter": "chatCompletions",
436
+ "adapter": "chat-completions",
437
437
  "model": "openai/gpt-4o-mini"
438
438
  }
439
439
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.9.18",
3
+ "version": "7.9.19",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.9.18",
36
- "@zuplo/core": "7.9.18",
37
- "@zuplo/runtime": "7.9.18",
38
- "@zuplo/test": "7.9.18"
35
+ "@zuplo/cli": "7.9.19",
36
+ "@zuplo/core": "7.9.19",
37
+ "@zuplo/runtime": "7.9.19",
38
+ "@zuplo/test": "7.9.19"
39
39
  }
40
40
  }