zuplo 7.9.17 → 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.
- package/docs/ai-gateway/integrations/claude-code.mdx +4 -4
- package/docs/ai-gateway/integrations/claude-desktop.mdx +9 -9
- package/docs/ai-gateway/integrations/codex.mdx +15 -15
- package/docs/ai-gateway/integrations/github-copilot.mdx +10 -10
- package/docs/ai-gateway/integrations/goose.mdx +2 -2
- package/docs/ai-gateway/user-apps.mdx +8 -8
- package/docs/dev-portal/zudoku/configuration/api-reference.md +16 -3
- package/docs/dev-portal/zudoku/configuration/build-configuration.mdx +7 -0
- package/docs/dev-portal/zudoku/configuration/overview.md +1 -1
- package/docs/dev-portal/zudoku/markdown/frontmatter.md +19 -0
- package/docs/dev-portal/zudoku/openapi-extensions/x-zudoku-request-box-enabled.md +47 -0
- package/docs/dev-portal/zudoku/openapi-extensions/x-zudoku-seo.md +3 -0
- package/docs/policies/_index.md +1 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/doc.md +522 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/intro.md +6 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/schema.json +175 -0
- package/docs/policies/ai-gateway-smart-router-inbound/doc.md +19 -19
- package/docs/policies/ai-gateway-smart-router-inbound/schema.json +10 -10
- package/package.json +5 -5
|
@@ -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: "
|
|
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: `
|
|
54
|
-
Selects `/v1/chat/completions`, `/v1/messages`, or
|
|
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": "
|
|
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: "
|
|
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: "
|
|
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. `
|
|
224
|
-
request and response contract; it does not add support for arbitrary
|
|
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: "
|
|
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 `
|
|
375
|
+
(its default `chat-completions` adapter, or `messages`, which sends it as
|
|
376
376
|
`system`) and `classifier.chatCompletions`.
|
|
377
|
-
`classifier.app.adapter: "
|
|
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: "
|
|
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: "
|
|
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": "
|
|
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": "
|
|
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": ["
|
|
145
|
-
"default": "
|
|
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.
|
|
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` `
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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.
|
|
36
|
-
"@zuplo/core": "7.9.
|
|
37
|
-
"@zuplo/runtime": "7.9.
|
|
38
|
-
"@zuplo/test": "7.9.
|
|
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
|
}
|