zuplo 7.9.16 → 7.9.17

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.
@@ -90,6 +90,11 @@ use the app's API key and API URL instead.
90
90
 
91
91
  `ANTHROPIC_AUTH_TOKEN` replaces your `claude.ai` login.
92
92
 
93
+ Add `"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1"` to `env` to add your
94
+ gateway's Claude models to the `/model` picker. See Claude Code's
95
+ [gateway model discovery](https://code.claude.com/docs/en/llm-gateway-protocol#model-discovery)
96
+ docs for details.
97
+
93
98
  ## Use your Claude subscription
94
99
 
95
100
  This approach is shown with an app. On the User App, turning on passthrough
@@ -99,6 +104,13 @@ subscription logins.
99
104
  Claude Code stays signed in to `claude.ai`. The gateway forwards that login to
100
105
  Anthropic and reads the app's API key from a separate `zp-gateway-api-key`
101
106
  header, so it still authenticates the app, runs its policies, and meters usage.
107
+ Model discovery (`CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY`) doesn't work with
108
+ a subscription login; list the models you need with the `ANTHROPIC_*_MODEL`
109
+ variables and Claude Code's
110
+ [`modelPicker`](https://code.claude.com/docs/en/settings-reference#modelpicker)
111
+ setting instead. **Put `modelPicker` in `~/.claude/settings.json` in your home
112
+ directory, not in a project's `.claude/settings.json`. Claude Code ignores it in
113
+ project settings.**
102
114
 
103
115
  ### Configure the API Key Authentication policy
104
116
 
@@ -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: "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: "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. |
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,8 +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) or `systemOne`. Selects
54
- `/v1/chat/completions` or `/v1/systemone` on that app.
53
+ - `adapter` — optional: `chatCompletions` (default), `messages`, or `systemOne`.
54
+ Selects `/v1/chat/completions`, `/v1/messages`, or `/v1/systemone` on that
55
+ app.
55
56
  - `model` — the `providerName/model` that app routes the classifier request to.
56
57
  - `apiKey` — optional. See
57
58
  [Authenticating the classifier app](#authenticating-the-classifier-app).
@@ -61,6 +62,54 @@ policy runs on. The classifier request never leaves the gateway, so that
61
62
  application needs no API key checked at all — put the Ensure Gateway Internal
62
63
  Invocation Only policy on it in place of API Key Authentication.
63
64
 
65
+ For Claude models registered with your gateway, use the same app path with the
66
+ Anthropic Messages API:
67
+
68
+ ```json
69
+ "classifier": {
70
+ "app": {
71
+ "appId": "$env(CLASSIFIER_APP_ID)",
72
+ "adapter": "messages",
73
+ "model": "anthropic/claude-haiku-4-5"
74
+ }
75
+ }
76
+ ```
77
+
78
+ Replace `anthropic` with your configured provider name. Smart Router sends the
79
+ classifier prompt as `system`, the user prompt as the only message, and the
80
+ profile schema as `output_config.format`, so Claude's reply is constrained to
81
+ that JSON schema. Both the model and the provider must support
82
+ [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs):
83
+
84
+ - **Model** — Claude Haiku 4.5 and the current Sonnet, Opus, and Fable models
85
+ support them.
86
+ - **Provider** — the provider's Messages endpoint must accept
87
+ `output_config.format`. Anthropic, Google Vertex AI, and Microsoft Foundry do.
88
+ Amazon Bedrock Mantle does not: its `/anthropic/v1/messages` endpoint rejects
89
+ `output_config.format` with a `400`, so every classification would fail. Use
90
+ another provider for the classifier app.
91
+
92
+ A model or provider without structured outputs rejects the request with a `400`,
93
+ which the policy logs and fails open on. The request allows up to 2,048 output
94
+ tokens, because thinking counts toward that limit on models that always think. A
95
+ reply cut off at that limit or at the model's context window, or a refusal, is
96
+ logged as a classifier error.
97
+
98
+ Use Claude Haiku 4.5 unless you've measured that a larger model classifies your
99
+ traffic better. Larger models such as Claude Opus 5.5 and Sonnet 5.5 always
100
+ think, which makes each classification slower and more expensive: at the 2,048
101
+ token limit, one classification on Claude Opus 5.5 can cost about $0.04. They
102
+ can also run past the default 8-second `classifierTimeoutMs`, which leaves the
103
+ request unclassified.
104
+
105
+ Anthropic compiles each new schema on first use and caches it for 24 hours. The
106
+ schema is built from your `intents`, so the first classification after you
107
+ change them, or after the cache expires, can be slower than usual. If it runs
108
+ past `classifierTimeoutMs`, that one request continues unclassified.
109
+
110
+ The `messages` adapter does not translate between providers: pick a model the
111
+ classifier app serves on `/v1/messages`.
112
+
64
113
  For Jev registered with your gateway, use the same app path with System One:
65
114
 
66
115
  ```json
@@ -169,10 +218,11 @@ which can echo the request back, prompt included.
169
218
 
170
219
  To migrate a direct section, register the provider with the gateway, create a
171
220
  classifier app for it, and replace the section with `classifier.app`. Use
172
- `adapter: "systemOne"` to replace `classifier.jev`. Existing `classifier.app`
173
- configurations continue to default to Chat Completions when `adapter` is
174
- omitted. `systemOne` uses the existing Jev request and response contract; it
175
- does not add support for arbitrary decision APIs.
221
+ `adapter: "systemOne"` to replace `classifier.jev`, and `adapter: "messages"`
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.
176
226
 
177
227
  ## Authenticating the classifier app
178
228
 
@@ -322,7 +372,8 @@ so it's never eligible for routing.
322
372
  ### Custom classifier prompt
323
373
 
324
374
  `classifierPrompt` replaces the built-in classifier prompt for `classifier.app`
325
- (its default or `chatCompletions` adapter) and `classifier.chatCompletions`.
375
+ (its default `chatCompletions` adapter, or `messages`, which sends it as
376
+ `system`) and `classifier.chatCompletions`.
326
377
  `classifier.app.adapter: "systemOne"` and `classifier.jev` take no prompt, so
327
378
  combining `classifierPrompt` with either is a configuration error. It accepts
328
379
  either a string or an array of lines:
@@ -77,8 +77,8 @@
77
77
  "classifier": {
78
78
  "app": {
79
79
  "appId": "$env(CLASSIFIER_APP_ID)",
80
- "adapter": "systemOne",
81
- "model": "jev/jev-latest"
80
+ "adapter": "messages",
81
+ "model": "anthropic/claude-haiku-4-5"
82
82
  }
83
83
  },
84
84
  "smartRoutingEnabled": true,
@@ -107,7 +107,7 @@
107
107
  "classifier": {
108
108
  "type": "object",
109
109
  "title": "Classifier",
110
- "description": "Where the prompt is classified. Set exactly one of `app`, `chatCompletions`, or `jev`. `app.adapter` selects the API that classifier app exposes. Setting more than one section is a configuration error.",
110
+ "description": "Where the prompt is classified. Set `app.adapter` to select the API that classifier app exposes.",
111
111
  "additionalProperties": false,
112
112
  "minProperties": 1,
113
113
  "maxProperties": 1,
@@ -128,7 +128,7 @@
128
128
  "model": {
129
129
  "type": "string",
130
130
  "title": "Classifier Model",
131
- "description": "The model (`providerName/model`) the classifier app uses to evaluate the user's request. `providerName` is the name of the provider as configured on the classifier app, not a fixed vendor id. E.g. `openai/gpt-4o-mini`, or `jev/jev-latest` when the Jev provider is named `jev`.",
131
+ "description": "The model (`providerName/model`) the classifier app uses to evaluate the user's request. `providerName` is the name of the provider as configured on the classifier app, not a fixed vendor id. E.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4-5` with the `messages` adapter, or `jev/jev-latest` when the Jev provider is named `jev`.",
132
132
  "pattern": "^[^/\\s]+/.+$",
133
133
  "examples": ["openai/gpt-4o-mini"]
134
134
  },
@@ -141,10 +141,10 @@
141
141
  },
142
142
  "adapter": {
143
143
  "type": "string",
144
- "enum": ["chatCompletions", "systemOne"],
144
+ "enum": ["chatCompletions", "messages", "systemOne"],
145
145
  "default": "chatCompletions",
146
146
  "title": "Classifier App Adapter",
147
- "description": "API contract exposed by the classifier app. chatCompletions calls /v1/chat/completions; 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. 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."
148
148
  }
149
149
  }
150
150
  },
@@ -279,7 +279,7 @@
279
279
  },
280
280
  "classifierPrompt": {
281
281
  "title": "Classifier Prompt",
282
- "description": "Prompt for classifier.app (its default or chatCompletions adapter) 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 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.",
283
283
  "oneOf": [
284
284
  {
285
285
  "type": "string",
@@ -414,8 +414,8 @@
414
414
  "classifier": {
415
415
  "app": {
416
416
  "appId": "$env(CLASSIFIER_APP_ID)",
417
- "adapter": "systemOne",
418
- "model": "jev/jev-latest"
417
+ "adapter": "messages",
418
+ "model": "anthropic/claude-haiku-4-5"
419
419
  }
420
420
  },
421
421
  "smartRoutingEnabled": true,
@@ -49,7 +49,7 @@
49
49
  "pattern": "^[\\t\\x20-\\x7E\\x80-\\xFF]+$",
50
50
  "examples": ["Not Found", "Unauthorized"],
51
51
  "x-advanced": true,
52
- "description": "The status text returned with `status`, such as `Not Found`. When it is omitted, the response uses the standard text for the status. Clients don't always receive it: HTTP/2 and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways always send the standard text."
52
+ "description": "The status text returned with `status`, such as `Not Found` for a `404`. When it is omitted, the response uses the standard text for the status. Clients don't always receive it: HTTP/2 and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways always send the standard text."
53
53
  },
54
54
  "detail": {
55
55
  "type": "string",
@@ -57,7 +57,13 @@
57
57
  "x-advanced": true,
58
58
  "description": "The `detail` of the problem response returned to a rejected request. Every caller outside the gateway receives it, so don't describe how the route is called. When it is omitted, the response has no `detail`, and a `404` matches the response for a route that does not exist."
59
59
  }
60
- }
60
+ },
61
+ "examples": [
62
+ {
63
+ "status": 404,
64
+ "statusText": "Not Found"
65
+ }
66
+ ]
61
67
  }
62
68
  },
63
69
  "examples": [
@@ -65,7 +71,7 @@
65
71
  "export": "InternalOnlyInboundPolicy",
66
72
  "module": "$import(@zuplo/runtime)",
67
73
  "options": {
68
- "status": 403,
74
+ "status": 404,
69
75
  "statusText": "Not Found"
70
76
  }
71
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.9.16",
3
+ "version": "7.9.17",
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.16",
36
- "@zuplo/core": "7.9.16",
37
- "@zuplo/runtime": "7.9.16",
38
- "@zuplo/test": "7.9.16"
35
+ "@zuplo/cli": "7.9.17",
36
+ "@zuplo/core": "7.9.17",
37
+ "@zuplo/runtime": "7.9.17",
38
+ "@zuplo/test": "7.9.17"
39
39
  }
40
40
  }