@kaana.ai/contracts 8.1.0 → 8.3.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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/catalogue.d.ts +28 -1
- package/dist/internal/catalogue.js +36 -2
- package/dist/internal/grant.d.ts +3 -0
- package/dist/internal/grant.js +8 -1
- package/dist/internal/realtime.d.ts +2 -2
- package/dist/internal/request.d.ts +2 -2
- package/dist/public/anthropicMessages.d.ts +284 -0
- package/dist/public/anthropicMessages.js +274 -0
- package/dist/public/chatCompletions.d.ts +2 -2
- package/dist/public/messages.d.ts +57 -2
- package/dist/public/messages.js +108 -1
- package/dist/public/realtime.d.ts +7 -7
- package/dist/public/responses.d.ts +2 -2
- package/dist/public/version.d.ts +15 -1
- package/dist/public/version.js +15 -1
- package/openapi.json +1274 -140
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /v1/messages` — the Anthropic-Messages-compatible surface.
|
|
3
|
+
*
|
|
4
|
+
* It speaks the flat Anthropic request, response and stream shapes so an
|
|
5
|
+
* unmodified Anthropic SDK or Claude Code works with
|
|
6
|
+
* `ANTHROPIC_BASE_URL=https://kaana.ai` and an `oxy_sk_…` key (sent as
|
|
7
|
+
* `x-api-key` or as `Authorization: Bearer`). Like `/v1/chat/completions` it
|
|
8
|
+
* gains NO Kaana-specific request field, and it is `.strict()` everywhere: a
|
|
9
|
+
* stock SDK sending a parameter Kaana does not implement is told so rather than
|
|
10
|
+
* having it silently dropped.
|
|
11
|
+
*
|
|
12
|
+
* ## What is refused, and why
|
|
13
|
+
*
|
|
14
|
+
* - **`cache_control`, anywhere** (on a block, a tool, `system` or the
|
|
15
|
+
* request). Kaana's usage units have no unit for a cache WRITE (see the
|
|
16
|
+
* Anthropic adapter's usage meter): a cache write is billed at a premium
|
|
17
|
+
* over input, so honouring the marker while folding the write into
|
|
18
|
+
* `input_tokens` would report less than the request cost. The marker is
|
|
19
|
+
* refused, by name and path, instead of being dropped.
|
|
20
|
+
* - **`thinking` other than `{ type: 'disabled' }`.** The contract's reasoning
|
|
21
|
+
* control is an effort (`low`, `medium`, `high`), not a token budget, and a
|
|
22
|
+
* thinking block has no part in a message: reasoning is private and is never
|
|
23
|
+
* returned. Reasoning effort is asked with `output_config.effort`, which
|
|
24
|
+
* maps one to one. A `thinking` or `redacted_thinking` block in a message is
|
|
25
|
+
* refused for the same reason.
|
|
26
|
+
* - **Server tools** (`web_search`, `bash`, `text_editor`, …), `mcp_servers`,
|
|
27
|
+
* `container`, `service_tier`, `disable_parallel_tool_use`, `document`
|
|
28
|
+
* blocks and any other member this file does not name.
|
|
29
|
+
* - **An image `url` source**: nothing fetches an image URL (ADR 0007); a
|
|
30
|
+
* `base64` source is carried inline under the image rules.
|
|
31
|
+
*
|
|
32
|
+
* `stop_reason: 'stop_sequence'` is never produced: the finish reason Kaana
|
|
33
|
+
* measures does not say WHICH stop sequence ended the turn, and `stop_sequence`
|
|
34
|
+
* would then have to name one. A turn that ended on a stop sequence reports
|
|
35
|
+
* `end_turn`.
|
|
36
|
+
*
|
|
37
|
+
* `metadata.user_id` is accepted and neither forwarded nor stored: it is an
|
|
38
|
+
* end-user identifier, and Kaana persists none.
|
|
39
|
+
*
|
|
40
|
+
* `usage` is derived from the measured units: `input_tokens`, `output_tokens`
|
|
41
|
+
* (reasoning included, as Anthropic counts it) and, only when the route
|
|
42
|
+
* measured it, `cache_read_input_tokens`. There is no `cache_creation_*`.
|
|
43
|
+
*/
|
|
44
|
+
import { z } from 'zod';
|
|
45
|
+
import { maxUtf16Length, minUtf16Length } from '../stringBounds.js';
|
|
46
|
+
import { reasoningEffortSchema } from './catalogue.js';
|
|
47
|
+
import { modelTargetSchema } from './responses.js';
|
|
48
|
+
const nonEmpty = (max) => z.string().check(minUtf16Length(1)).check(maxUtf16Length(max));
|
|
49
|
+
const textBlockSchema = z.object({ type: z.literal('text'), text: z.string() }).strict();
|
|
50
|
+
const imageBlockSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
type: z.literal('image'),
|
|
53
|
+
source: z.discriminatedUnion('type', [
|
|
54
|
+
z
|
|
55
|
+
.object({
|
|
56
|
+
type: z.literal('base64'),
|
|
57
|
+
media_type: nonEmpty(255),
|
|
58
|
+
data: z.string().check(minUtf16Length(1)),
|
|
59
|
+
})
|
|
60
|
+
.strict(),
|
|
61
|
+
/** Parses so the refusal can name it; Kaana fetches no image URL. */
|
|
62
|
+
z.object({ type: z.literal('url'), url: nonEmpty(4096) }).strict(),
|
|
63
|
+
]),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const toolUseBlockSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
type: z.literal('tool_use'),
|
|
69
|
+
id: nonEmpty(128),
|
|
70
|
+
name: nonEmpty(128),
|
|
71
|
+
input: z.record(z.string(), z.unknown()),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
const toolResultBlockSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
type: z.literal('tool_result'),
|
|
77
|
+
tool_use_id: nonEmpty(128),
|
|
78
|
+
content: z
|
|
79
|
+
.union([z.string(), z.array(z.union([textBlockSchema, imageBlockSchema]))])
|
|
80
|
+
.optional(),
|
|
81
|
+
/**
|
|
82
|
+
* Accepted. The contract's tool message has no error flag, so the result's
|
|
83
|
+
* own content is what the model reads; the flag is not forwarded.
|
|
84
|
+
*/
|
|
85
|
+
is_error: z.boolean().optional(),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
const messageSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
role: z.enum(['user', 'assistant']),
|
|
91
|
+
content: z.union([
|
|
92
|
+
z.string(),
|
|
93
|
+
z
|
|
94
|
+
.array(z.union([textBlockSchema, imageBlockSchema, toolUseBlockSchema, toolResultBlockSchema]))
|
|
95
|
+
.min(1),
|
|
96
|
+
]),
|
|
97
|
+
})
|
|
98
|
+
.strict()
|
|
99
|
+
.superRefine((message, ctx) => {
|
|
100
|
+
if (typeof message.content === 'string')
|
|
101
|
+
return;
|
|
102
|
+
for (const [index, block] of message.content.entries()) {
|
|
103
|
+
const allowed = message.role === 'assistant'
|
|
104
|
+
? block.type === 'text' || block.type === 'tool_use'
|
|
105
|
+
: block.type !== 'tool_use';
|
|
106
|
+
if (!allowed) {
|
|
107
|
+
ctx.addIssue({
|
|
108
|
+
code: 'custom',
|
|
109
|
+
path: ['content', index, 'type'],
|
|
110
|
+
message: `a ${message.role} message does not carry a ${block.type} block`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const toolSchema = z
|
|
116
|
+
.object({
|
|
117
|
+
type: z.literal('custom').optional(),
|
|
118
|
+
name: nonEmpty(128),
|
|
119
|
+
description: z.string().check(maxUtf16Length(32768)).optional(),
|
|
120
|
+
input_schema: z.record(z.string(), z.unknown()),
|
|
121
|
+
strict: z.boolean().optional(),
|
|
122
|
+
})
|
|
123
|
+
.strict();
|
|
124
|
+
const toolChoiceSchema = z.discriminatedUnion('type', [
|
|
125
|
+
z.object({ type: z.literal('auto') }).strict(),
|
|
126
|
+
z.object({ type: z.literal('any') }).strict(),
|
|
127
|
+
z.object({ type: z.literal('none') }).strict(),
|
|
128
|
+
z.object({ type: z.literal('tool'), name: nonEmpty(128) }).strict(),
|
|
129
|
+
]);
|
|
130
|
+
export const anthropicMessagesRequestSchema = z
|
|
131
|
+
.object({
|
|
132
|
+
/** A model reference (`publisher/model[@revision]`) or a power level. */
|
|
133
|
+
model: modelTargetSchema,
|
|
134
|
+
/** Required, as Anthropic requires it. */
|
|
135
|
+
max_tokens: z.number().int().positive().safe(),
|
|
136
|
+
messages: z.array(messageSchema).min(1),
|
|
137
|
+
system: z.union([z.string(), z.array(textBlockSchema)]).optional(),
|
|
138
|
+
temperature: z.number().min(0).max(1).optional(),
|
|
139
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
140
|
+
top_k: z.number().int().positive().safe().optional(),
|
|
141
|
+
stop_sequences: z.array(nonEmpty(256)).max(8).optional(),
|
|
142
|
+
stream: z.boolean().optional(),
|
|
143
|
+
tools: z.array(toolSchema).max(128).optional(),
|
|
144
|
+
tool_choice: toolChoiceSchema.optional(),
|
|
145
|
+
/** Only "off" is accepted: see the file comment. */
|
|
146
|
+
thinking: z
|
|
147
|
+
.object({ type: z.literal('disabled') })
|
|
148
|
+
.strict()
|
|
149
|
+
.optional(),
|
|
150
|
+
/** `effort` is the contract's reasoning effort; no other member is carried. */
|
|
151
|
+
output_config: z.object({ effort: reasoningEffortSchema.optional() }).strict().optional(),
|
|
152
|
+
/** Accepted and not forwarded or stored. */
|
|
153
|
+
metadata: z
|
|
154
|
+
.object({ user_id: z.string().check(maxUtf16Length(512)).nullable().optional() })
|
|
155
|
+
.strict()
|
|
156
|
+
.optional(),
|
|
157
|
+
})
|
|
158
|
+
.strict()
|
|
159
|
+
.superRefine((request, ctx) => {
|
|
160
|
+
if (request.tool_choice !== undefined && (request.tools ?? []).length === 0) {
|
|
161
|
+
ctx.addIssue({
|
|
162
|
+
code: 'custom',
|
|
163
|
+
path: ['tool_choice'],
|
|
164
|
+
message: 'a tool choice requires at least one tool definition',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const usageSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
input_tokens: z.number().int().nonnegative(),
|
|
171
|
+
output_tokens: z.number().int().nonnegative(),
|
|
172
|
+
/** Present only when the route measured a cache read. */
|
|
173
|
+
cache_read_input_tokens: z.number().int().nonnegative().optional(),
|
|
174
|
+
})
|
|
175
|
+
.strict();
|
|
176
|
+
const stopReasonSchema = z.enum(['end_turn', 'max_tokens', 'tool_use', 'refusal']);
|
|
177
|
+
const outputBlockSchema = z.discriminatedUnion('type', [
|
|
178
|
+
textBlockSchema,
|
|
179
|
+
toolUseBlockSchema.extend({ input: z.record(z.string(), z.unknown()) }),
|
|
180
|
+
]);
|
|
181
|
+
const messageBodySchema = z.object({
|
|
182
|
+
id: nonEmpty(256),
|
|
183
|
+
type: z.literal('message'),
|
|
184
|
+
role: z.literal('assistant'),
|
|
185
|
+
/** The revision that served the request. */
|
|
186
|
+
model: nonEmpty(256),
|
|
187
|
+
});
|
|
188
|
+
/** The non-streaming body. The request id rides in `Kaana-Request-Id` and `Request-Id`. */
|
|
189
|
+
export const anthropicMessageSchema = messageBodySchema
|
|
190
|
+
.extend({
|
|
191
|
+
content: z.array(outputBlockSchema),
|
|
192
|
+
stop_reason: stopReasonSchema,
|
|
193
|
+
stop_sequence: z.literal(null),
|
|
194
|
+
usage: usageSchema,
|
|
195
|
+
})
|
|
196
|
+
.strict();
|
|
197
|
+
/** The error body, and the data of a stream's `event: error` frame. */
|
|
198
|
+
export const anthropicErrorSchema = z
|
|
199
|
+
.object({
|
|
200
|
+
type: z.literal('error'),
|
|
201
|
+
error: z
|
|
202
|
+
.object({
|
|
203
|
+
type: z.enum([
|
|
204
|
+
'invalid_request_error',
|
|
205
|
+
'authentication_error',
|
|
206
|
+
'billing_error',
|
|
207
|
+
'permission_error',
|
|
208
|
+
'not_found_error',
|
|
209
|
+
'request_too_large',
|
|
210
|
+
'rate_limit_error',
|
|
211
|
+
'timeout_error',
|
|
212
|
+
'overloaded_error',
|
|
213
|
+
'api_error',
|
|
214
|
+
]),
|
|
215
|
+
message: nonEmpty(4096),
|
|
216
|
+
})
|
|
217
|
+
.strict(),
|
|
218
|
+
request_id: nonEmpty(256).optional(),
|
|
219
|
+
})
|
|
220
|
+
.strict();
|
|
221
|
+
/**
|
|
222
|
+
* One frame of a streamed message: `event: <type>` and this as its data. The
|
|
223
|
+
* input tokens are known only when the route reports usage, so `message_start`
|
|
224
|
+
* carries zeros and the final `message_delta` the measured totals. A stream that
|
|
225
|
+
* fails after its first frame ends with an `error` frame and no `message_stop`.
|
|
226
|
+
*/
|
|
227
|
+
export const anthropicStreamEventSchema = z.discriminatedUnion('type', [
|
|
228
|
+
z
|
|
229
|
+
.object({
|
|
230
|
+
type: z.literal('message_start'),
|
|
231
|
+
message: messageBodySchema
|
|
232
|
+
.extend({
|
|
233
|
+
content: z.array(outputBlockSchema).length(0),
|
|
234
|
+
stop_reason: z.literal(null),
|
|
235
|
+
stop_sequence: z.literal(null),
|
|
236
|
+
usage: usageSchema,
|
|
237
|
+
})
|
|
238
|
+
.strict(),
|
|
239
|
+
})
|
|
240
|
+
.strict(),
|
|
241
|
+
z
|
|
242
|
+
.object({
|
|
243
|
+
type: z.literal('content_block_start'),
|
|
244
|
+
index: z.number().int().nonnegative(),
|
|
245
|
+
content_block: z.discriminatedUnion('type', [
|
|
246
|
+
z.object({ type: z.literal('text'), text: z.literal('') }).strict(),
|
|
247
|
+
toolUseBlockSchema.extend({ input: z.object({}).strict() }),
|
|
248
|
+
]),
|
|
249
|
+
})
|
|
250
|
+
.strict(),
|
|
251
|
+
z
|
|
252
|
+
.object({
|
|
253
|
+
type: z.literal('content_block_delta'),
|
|
254
|
+
index: z.number().int().nonnegative(),
|
|
255
|
+
delta: z.discriminatedUnion('type', [
|
|
256
|
+
z.object({ type: z.literal('text_delta'), text: z.string() }).strict(),
|
|
257
|
+
z.object({ type: z.literal('input_json_delta'), partial_json: z.string() }).strict(),
|
|
258
|
+
]),
|
|
259
|
+
})
|
|
260
|
+
.strict(),
|
|
261
|
+
z
|
|
262
|
+
.object({ type: z.literal('content_block_stop'), index: z.number().int().nonnegative() })
|
|
263
|
+
.strict(),
|
|
264
|
+
z
|
|
265
|
+
.object({
|
|
266
|
+
type: z.literal('message_delta'),
|
|
267
|
+
delta: z.object({ stop_reason: stopReasonSchema, stop_sequence: z.literal(null) }).strict(),
|
|
268
|
+
usage: usageSchema,
|
|
269
|
+
})
|
|
270
|
+
.strict(),
|
|
271
|
+
z.object({ type: z.literal('message_stop') }).strict(),
|
|
272
|
+
z.object({ type: z.literal('ping') }).strict(),
|
|
273
|
+
anthropicErrorSchema,
|
|
274
|
+
]);
|
|
@@ -21,9 +21,9 @@ import { z } from 'zod';
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const chatCompletionMessageSchema: z.ZodObject<{
|
|
23
23
|
role: z.ZodEnum<{
|
|
24
|
-
user: "user";
|
|
25
24
|
system: "system";
|
|
26
25
|
developer: "developer";
|
|
26
|
+
user: "user";
|
|
27
27
|
assistant: "assistant";
|
|
28
28
|
tool: "tool";
|
|
29
29
|
}>;
|
|
@@ -93,9 +93,9 @@ export declare const chatCompletionsRequestSchema: z.ZodObject<{
|
|
|
93
93
|
}>]>;
|
|
94
94
|
messages: z.ZodArray<z.ZodObject<{
|
|
95
95
|
role: z.ZodEnum<{
|
|
96
|
-
user: "user";
|
|
97
96
|
system: "system";
|
|
98
97
|
developer: "developer";
|
|
98
|
+
user: "user";
|
|
99
99
|
assistant: "assistant";
|
|
100
100
|
tool: "tool";
|
|
101
101
|
}>;
|
|
@@ -95,6 +95,61 @@ export declare const inferenceContentPartSchema: z.ZodDiscriminatedUnion<[z.ZodO
|
|
|
95
95
|
type: z.ZodLiteral<"refusal">;
|
|
96
96
|
text: z.ZodString;
|
|
97
97
|
}, z.core.$strict>], "type">;
|
|
98
|
+
/**
|
|
99
|
+
* The image formats a request may carry. Every provider that serves image input
|
|
100
|
+
* on a Kaana route reads these four: Anthropic (`image/jpeg`, `image/png`,
|
|
101
|
+
* `image/gif`, `image/webp`) and OpenAI (PNG, JPEG, WEBP, non-animated GIF).
|
|
102
|
+
*/
|
|
103
|
+
export declare const INPUT_IMAGE_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
104
|
+
/**
|
|
105
|
+
* The most image parts one request may carry, across every message. Above 20
|
|
106
|
+
* images Anthropic applies a stricter per-image dimension limit to EVERY image
|
|
107
|
+
* of the request, so 20 is the most a request can carry and keep one rule per
|
|
108
|
+
* image on every route.
|
|
109
|
+
*/
|
|
110
|
+
export declare const MAX_INPUT_IMAGES = 20;
|
|
111
|
+
/**
|
|
112
|
+
* The most decoded bytes one image may carry: 3,750,000, whose base64 is
|
|
113
|
+
* 5,000,000 characters, the smallest per-image limit a provider serving these
|
|
114
|
+
* routes documents (Anthropic on Amazon Bedrock: 5 MB base64-encoded).
|
|
115
|
+
*/
|
|
116
|
+
export declare const MAX_INPUT_IMAGE_BYTES = 3750000;
|
|
117
|
+
/** Why one image part cannot be carried, before anything is held. Never the image itself. */
|
|
118
|
+
export interface ImageInputIssue {
|
|
119
|
+
/** The message, as its index in the request's message list. */
|
|
120
|
+
readonly messageIndex: number;
|
|
121
|
+
/** The part, as its index in that message's content; absent for the request-wide count. */
|
|
122
|
+
readonly partIndex?: number;
|
|
123
|
+
/** The field at fault, relative to the part. */
|
|
124
|
+
readonly field: 'role' | 'source' | 'source.mediaType' | 'source.data' | 'count';
|
|
125
|
+
readonly message: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check the image parts of a conversation against the contract's image rules
|
|
129
|
+
* (Kaana ADR 0007). Kaana refuses a request with `invalid_request` and the first
|
|
130
|
+
* issue BEFORE it asks Oxy for a grant; Oxy applies the same rules on its own
|
|
131
|
+
* edge; a client may run it first.
|
|
132
|
+
*
|
|
133
|
+
* - an image is `inline`: no component fetches an image URL, and a `url`
|
|
134
|
+
* source is never forwarded to a provider to fetch either;
|
|
135
|
+
* - its media type is one of {@link INPUT_IMAGE_MEDIA_TYPES};
|
|
136
|
+
* - its data is canonical base64 of at most {@link MAX_INPUT_IMAGE_BYTES} bytes;
|
|
137
|
+
* - it rides on a `user` or `tool` message;
|
|
138
|
+
* - the request carries at most {@link MAX_INPUT_IMAGES} of them.
|
|
139
|
+
*
|
|
140
|
+
* Returns the number of image parts and every issue, in message order. An issue
|
|
141
|
+
* names where and why, never the bytes: an image is payload and is never echoed
|
|
142
|
+
* into an error, a log line or a record.
|
|
143
|
+
*/
|
|
144
|
+
export declare function checkImageInput(messages: readonly InferenceMessage[]): {
|
|
145
|
+
readonly images: number;
|
|
146
|
+
readonly issues: ImageInputIssue[];
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The decoded length of canonical base64 (RFC 4648 section 4, padded, no line
|
|
150
|
+
* breaks), or `undefined` when the text is not that. Counted, never decoded.
|
|
151
|
+
*/
|
|
152
|
+
export declare function base64DecodedLength(data: string): number | undefined;
|
|
98
153
|
/**
|
|
99
154
|
* A tool call an assistant made, in the normalized form.
|
|
100
155
|
*
|
|
@@ -108,9 +163,9 @@ export declare const inferenceToolCallSchema: z.ZodObject<{
|
|
|
108
163
|
arguments: z.ZodString;
|
|
109
164
|
}, z.core.$strict>;
|
|
110
165
|
export declare const inferenceMessageRoleSchema: z.ZodEnum<{
|
|
111
|
-
user: "user";
|
|
112
166
|
system: "system";
|
|
113
167
|
developer: "developer";
|
|
168
|
+
user: "user";
|
|
114
169
|
assistant: "assistant";
|
|
115
170
|
tool: "tool";
|
|
116
171
|
}>;
|
|
@@ -124,9 +179,9 @@ export declare const inferenceMessageRoleSchema: z.ZodEnum<{
|
|
|
124
179
|
*/
|
|
125
180
|
export declare const inferenceMessageSchema: z.ZodObject<{
|
|
126
181
|
role: z.ZodEnum<{
|
|
127
|
-
user: "user";
|
|
128
182
|
system: "system";
|
|
129
183
|
developer: "developer";
|
|
184
|
+
user: "user";
|
|
130
185
|
assistant: "assistant";
|
|
131
186
|
tool: "tool";
|
|
132
187
|
}>;
|
package/dist/public/messages.js
CHANGED
|
@@ -86,6 +86,113 @@ export const inferenceContentPartSchema = z.discriminatedUnion('type', [
|
|
|
86
86
|
*/
|
|
87
87
|
z.object({ type: z.literal('refusal'), text: z.string() }).strict(),
|
|
88
88
|
]);
|
|
89
|
+
/* -------------------------------------------------------------------------- */
|
|
90
|
+
/* Image input (contract set 8.2.0, Kaana ADR 0007) */
|
|
91
|
+
/* -------------------------------------------------------------------------- */
|
|
92
|
+
/**
|
|
93
|
+
* The image formats a request may carry. Every provider that serves image input
|
|
94
|
+
* on a Kaana route reads these four: Anthropic (`image/jpeg`, `image/png`,
|
|
95
|
+
* `image/gif`, `image/webp`) and OpenAI (PNG, JPEG, WEBP, non-animated GIF).
|
|
96
|
+
*/
|
|
97
|
+
export const INPUT_IMAGE_MEDIA_TYPES = [
|
|
98
|
+
'image/png',
|
|
99
|
+
'image/jpeg',
|
|
100
|
+
'image/webp',
|
|
101
|
+
'image/gif',
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* The most image parts one request may carry, across every message. Above 20
|
|
105
|
+
* images Anthropic applies a stricter per-image dimension limit to EVERY image
|
|
106
|
+
* of the request, so 20 is the most a request can carry and keep one rule per
|
|
107
|
+
* image on every route.
|
|
108
|
+
*/
|
|
109
|
+
export const MAX_INPUT_IMAGES = 20;
|
|
110
|
+
/**
|
|
111
|
+
* The most decoded bytes one image may carry: 3,750,000, whose base64 is
|
|
112
|
+
* 5,000,000 characters, the smallest per-image limit a provider serving these
|
|
113
|
+
* routes documents (Anthropic on Amazon Bedrock: 5 MB base64-encoded).
|
|
114
|
+
*/
|
|
115
|
+
export const MAX_INPUT_IMAGE_BYTES = 3_750_000;
|
|
116
|
+
/**
|
|
117
|
+
* Check the image parts of a conversation against the contract's image rules
|
|
118
|
+
* (Kaana ADR 0007). Kaana refuses a request with `invalid_request` and the first
|
|
119
|
+
* issue BEFORE it asks Oxy for a grant; Oxy applies the same rules on its own
|
|
120
|
+
* edge; a client may run it first.
|
|
121
|
+
*
|
|
122
|
+
* - an image is `inline`: no component fetches an image URL, and a `url`
|
|
123
|
+
* source is never forwarded to a provider to fetch either;
|
|
124
|
+
* - its media type is one of {@link INPUT_IMAGE_MEDIA_TYPES};
|
|
125
|
+
* - its data is canonical base64 of at most {@link MAX_INPUT_IMAGE_BYTES} bytes;
|
|
126
|
+
* - it rides on a `user` or `tool` message;
|
|
127
|
+
* - the request carries at most {@link MAX_INPUT_IMAGES} of them.
|
|
128
|
+
*
|
|
129
|
+
* Returns the number of image parts and every issue, in message order. An issue
|
|
130
|
+
* names where and why, never the bytes: an image is payload and is never echoed
|
|
131
|
+
* into an error, a log line or a record.
|
|
132
|
+
*/
|
|
133
|
+
export function checkImageInput(messages) {
|
|
134
|
+
const issues = [];
|
|
135
|
+
let images = 0;
|
|
136
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
137
|
+
for (const [partIndex, part] of message.content.entries()) {
|
|
138
|
+
if (part.type !== 'image')
|
|
139
|
+
continue;
|
|
140
|
+
images += 1;
|
|
141
|
+
const at = { messageIndex, partIndex };
|
|
142
|
+
if (message.role !== 'user' && message.role !== 'tool') {
|
|
143
|
+
issues.push({ ...at, field: 'role', message: 'an image rides on a user or tool message' });
|
|
144
|
+
}
|
|
145
|
+
const source = part.source;
|
|
146
|
+
if (source.kind !== 'inline') {
|
|
147
|
+
issues.push({
|
|
148
|
+
...at,
|
|
149
|
+
field: 'source',
|
|
150
|
+
message: 'an image is sent inline as base64; no image URL is fetched',
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!INPUT_IMAGE_MEDIA_TYPES.includes(source.mediaType)) {
|
|
155
|
+
issues.push({
|
|
156
|
+
...at,
|
|
157
|
+
field: 'source.mediaType',
|
|
158
|
+
message: `an image is one of ${INPUT_IMAGE_MEDIA_TYPES.join(', ')}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const bytes = base64DecodedLength(source.data);
|
|
162
|
+
if (bytes === undefined) {
|
|
163
|
+
issues.push({ ...at, field: 'source.data', message: 'image data is canonical base64' });
|
|
164
|
+
}
|
|
165
|
+
else if (bytes > MAX_INPUT_IMAGE_BYTES) {
|
|
166
|
+
issues.push({
|
|
167
|
+
...at,
|
|
168
|
+
field: 'source.data',
|
|
169
|
+
message: `an image is at most ${MAX_INPUT_IMAGE_BYTES} bytes`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (images > MAX_INPUT_IMAGES) {
|
|
175
|
+
issues.push({
|
|
176
|
+
messageIndex: messages.length - 1,
|
|
177
|
+
field: 'count',
|
|
178
|
+
message: `a request carries at most ${MAX_INPUT_IMAGES} images`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return { images, issues };
|
|
182
|
+
}
|
|
183
|
+
const BASE64_BODY = /^[A-Za-z0-9+/]*$/;
|
|
184
|
+
/**
|
|
185
|
+
* The decoded length of canonical base64 (RFC 4648 section 4, padded, no line
|
|
186
|
+
* breaks), or `undefined` when the text is not that. Counted, never decoded.
|
|
187
|
+
*/
|
|
188
|
+
export function base64DecodedLength(data) {
|
|
189
|
+
if (data.length === 0 || data.length % 4 !== 0)
|
|
190
|
+
return undefined;
|
|
191
|
+
const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;
|
|
192
|
+
if (!BASE64_BODY.test(data.slice(0, data.length - padding)))
|
|
193
|
+
return undefined;
|
|
194
|
+
return (data.length / 4) * 3 - padding;
|
|
195
|
+
}
|
|
89
196
|
/**
|
|
90
197
|
* A tool call an assistant made, in the normalized form.
|
|
91
198
|
*
|
|
@@ -189,7 +296,7 @@ export const toolDefinitionSchema = z
|
|
|
189
296
|
.object({
|
|
190
297
|
type: z.literal('function'),
|
|
191
298
|
name: z.string().check(minUtf16Length(1)).check(maxUtf16Length(128)),
|
|
192
|
-
description: z.string().check(maxUtf16Length(
|
|
299
|
+
description: z.string().check(maxUtf16Length(32768)).optional(),
|
|
193
300
|
parameters: z.record(z.string(), z.unknown()),
|
|
194
301
|
/** Ask the provider to enforce the schema, where it supports enforcement. */
|
|
195
302
|
strict: z.boolean().optional(),
|
|
@@ -284,8 +284,8 @@ export declare const realtimeConversationItemSchema: z.ZodDiscriminatedUnion<[z.
|
|
|
284
284
|
type: z.ZodLiteral<"message">;
|
|
285
285
|
itemId: z.ZodOptional<z.ZodString>;
|
|
286
286
|
role: z.ZodEnum<{
|
|
287
|
-
user: "user";
|
|
288
287
|
system: "system";
|
|
288
|
+
user: "user";
|
|
289
289
|
assistant: "assistant";
|
|
290
290
|
}>;
|
|
291
291
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -401,8 +401,8 @@ export declare const realtimeItemCreateCommandSchema: z.ZodObject<{
|
|
|
401
401
|
type: z.ZodLiteral<"message">;
|
|
402
402
|
itemId: z.ZodOptional<z.ZodString>;
|
|
403
403
|
role: z.ZodEnum<{
|
|
404
|
-
user: "user";
|
|
405
404
|
system: "system";
|
|
405
|
+
user: "user";
|
|
406
406
|
assistant: "assistant";
|
|
407
407
|
}>;
|
|
408
408
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -591,8 +591,8 @@ export declare const realtimeClientCommandSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
591
591
|
type: z.ZodLiteral<"message">;
|
|
592
592
|
itemId: z.ZodOptional<z.ZodString>;
|
|
593
593
|
role: z.ZodEnum<{
|
|
594
|
-
user: "user";
|
|
595
594
|
system: "system";
|
|
595
|
+
user: "user";
|
|
596
596
|
assistant: "assistant";
|
|
597
597
|
}>;
|
|
598
598
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -796,8 +796,8 @@ export declare const realtimeItemAddedEventSchema: z.ZodObject<{
|
|
|
796
796
|
type: z.ZodLiteral<"message">;
|
|
797
797
|
itemId: z.ZodOptional<z.ZodString>;
|
|
798
798
|
role: z.ZodEnum<{
|
|
799
|
-
user: "user";
|
|
800
799
|
system: "system";
|
|
800
|
+
user: "user";
|
|
801
801
|
assistant: "assistant";
|
|
802
802
|
}>;
|
|
803
803
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -848,8 +848,8 @@ export declare const realtimeItemDoneEventSchema: z.ZodObject<{
|
|
|
848
848
|
type: z.ZodLiteral<"message">;
|
|
849
849
|
itemId: z.ZodOptional<z.ZodString>;
|
|
850
850
|
role: z.ZodEnum<{
|
|
851
|
-
user: "user";
|
|
852
851
|
system: "system";
|
|
852
|
+
user: "user";
|
|
853
853
|
assistant: "assistant";
|
|
854
854
|
}>;
|
|
855
855
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -1655,8 +1655,8 @@ export declare const realtimePublicServerEventSchema: z.ZodDiscriminatedUnion<[z
|
|
|
1655
1655
|
type: z.ZodLiteral<"message">;
|
|
1656
1656
|
itemId: z.ZodOptional<z.ZodString>;
|
|
1657
1657
|
role: z.ZodEnum<{
|
|
1658
|
-
user: "user";
|
|
1659
1658
|
system: "system";
|
|
1659
|
+
user: "user";
|
|
1660
1660
|
assistant: "assistant";
|
|
1661
1661
|
}>;
|
|
1662
1662
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -1706,8 +1706,8 @@ export declare const realtimePublicServerEventSchema: z.ZodDiscriminatedUnion<[z
|
|
|
1706
1706
|
type: z.ZodLiteral<"message">;
|
|
1707
1707
|
itemId: z.ZodOptional<z.ZodString>;
|
|
1708
1708
|
role: z.ZodEnum<{
|
|
1709
|
-
user: "user";
|
|
1710
1709
|
system: "system";
|
|
1710
|
+
user: "user";
|
|
1711
1711
|
assistant: "assistant";
|
|
1712
1712
|
}>;
|
|
1713
1713
|
content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -44,9 +44,9 @@ export declare const responsesRequestSchema: z.ZodObject<{
|
|
|
44
44
|
}>]>>;
|
|
45
45
|
input: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodObject<{
|
|
46
46
|
role: z.ZodEnum<{
|
|
47
|
-
user: "user";
|
|
48
47
|
system: "system";
|
|
49
48
|
developer: "developer";
|
|
49
|
+
user: "user";
|
|
50
50
|
assistant: "assistant";
|
|
51
51
|
tool: "tool";
|
|
52
52
|
}>;
|
|
@@ -164,9 +164,9 @@ export declare const responseSchema: z.ZodObject<{
|
|
|
164
164
|
}>;
|
|
165
165
|
output: z.ZodArray<z.ZodObject<{
|
|
166
166
|
role: z.ZodEnum<{
|
|
167
|
-
user: "user";
|
|
168
167
|
system: "system";
|
|
169
168
|
developer: "developer";
|
|
169
|
+
user: "user";
|
|
170
170
|
assistant: "assistant";
|
|
171
171
|
tool: "tool";
|
|
172
172
|
}>;
|
package/dist/public/version.d.ts
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
* change to, say, the catalogue reject every in-flight inference request; the
|
|
101
101
|
* per-shape `schemaVersion` is what a message is validated against.
|
|
102
102
|
*/
|
|
103
|
-
export declare const INFERENCE_CONTRACT_VERSION = "8.
|
|
103
|
+
export declare const INFERENCE_CONTRACT_VERSION = "8.3.0";
|
|
104
104
|
/**
|
|
105
105
|
* Contract set 4.0.0 (Kaana ADR 0002) is the first published as
|
|
106
106
|
* `@kaana.ai/contracts`. MAJOR because the set changed hands and split in two:
|
|
@@ -187,6 +187,20 @@ export declare const INFERENCE_CONTRACT_VERSION = "8.1.0";
|
|
|
187
187
|
* `transcript.interim` event it asks for, the provisional text of an input
|
|
188
188
|
* item that the item's deltas and its `transcript.done` supersede. Any other
|
|
189
189
|
* kind asking for interim text is refused. Nothing in 8.0.0 changes.
|
|
190
|
+
*
|
|
191
|
+
* Contract set 8.2.0 (Kaana ADR 0007) is additive: image input. The rules an
|
|
192
|
+
* image part answers to (`checkImageInput`, `INPUT_IMAGE_MEDIA_TYPES`,
|
|
193
|
+
* `MAX_INPUT_IMAGES`, `MAX_INPUT_IMAGE_BYTES`), the internal declared model's
|
|
194
|
+
* `imageInput` (the most input tokens one image costs on the line) and the
|
|
195
|
+
* grant fact `inputImages`. Every 8.1.0 message parses the same; a consumer on
|
|
196
|
+
* 8.1.0 or earlier refuses a declared catalogue or an authorization carrying
|
|
197
|
+
* what is new, so Oxy reads them before Kaana publishes them.
|
|
198
|
+
*
|
|
199
|
+
* Contract set 8.3.0 is additive: the Anthropic-Messages-compatible
|
|
200
|
+
* `POST /v1/messages` (`anthropicMessagesRequestSchema`,
|
|
201
|
+
* `anthropicMessageSchema`, `anthropicStreamEventSchema`,
|
|
202
|
+
* `anthropicErrorSchema`). Nothing in 8.2.0 changes, and the signed grant and
|
|
203
|
+
* envelope are untouched: the endpoint is served as the `responses` operation.
|
|
190
204
|
*/
|
|
191
205
|
/** The public API's one origin and version. */
|
|
192
206
|
export declare const KAANA_API_BASE_URL = "https://kaana.ai/v1";
|
package/dist/public/version.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
* change to, say, the catalogue reject every in-flight inference request; the
|
|
101
101
|
* per-shape `schemaVersion` is what a message is validated against.
|
|
102
102
|
*/
|
|
103
|
-
export const INFERENCE_CONTRACT_VERSION = '8.
|
|
103
|
+
export const INFERENCE_CONTRACT_VERSION = '8.3.0';
|
|
104
104
|
/**
|
|
105
105
|
* Contract set 4.0.0 (Kaana ADR 0002) is the first published as
|
|
106
106
|
* `@kaana.ai/contracts`. MAJOR because the set changed hands and split in two:
|
|
@@ -187,6 +187,20 @@ export const INFERENCE_CONTRACT_VERSION = '8.1.0';
|
|
|
187
187
|
* `transcript.interim` event it asks for, the provisional text of an input
|
|
188
188
|
* item that the item's deltas and its `transcript.done` supersede. Any other
|
|
189
189
|
* kind asking for interim text is refused. Nothing in 8.0.0 changes.
|
|
190
|
+
*
|
|
191
|
+
* Contract set 8.2.0 (Kaana ADR 0007) is additive: image input. The rules an
|
|
192
|
+
* image part answers to (`checkImageInput`, `INPUT_IMAGE_MEDIA_TYPES`,
|
|
193
|
+
* `MAX_INPUT_IMAGES`, `MAX_INPUT_IMAGE_BYTES`), the internal declared model's
|
|
194
|
+
* `imageInput` (the most input tokens one image costs on the line) and the
|
|
195
|
+
* grant fact `inputImages`. Every 8.1.0 message parses the same; a consumer on
|
|
196
|
+
* 8.1.0 or earlier refuses a declared catalogue or an authorization carrying
|
|
197
|
+
* what is new, so Oxy reads them before Kaana publishes them.
|
|
198
|
+
*
|
|
199
|
+
* Contract set 8.3.0 is additive: the Anthropic-Messages-compatible
|
|
200
|
+
* `POST /v1/messages` (`anthropicMessagesRequestSchema`,
|
|
201
|
+
* `anthropicMessageSchema`, `anthropicStreamEventSchema`,
|
|
202
|
+
* `anthropicErrorSchema`). Nothing in 8.2.0 changes, and the signed grant and
|
|
203
|
+
* envelope are untouched: the endpoint is served as the `responses` operation.
|
|
190
204
|
*/
|
|
191
205
|
/** The public API's one origin and version. */
|
|
192
206
|
export const KAANA_API_BASE_URL = 'https://kaana.ai/v1';
|