@kaana.ai/contracts 8.1.0 → 8.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.
@@ -562,6 +562,26 @@ export declare const modelCatalogueEntrySchema: z.ZodObject<{
562
562
  ultra: "ultra";
563
563
  }>>;
564
564
  }, z.core.$strip>;
565
+ /**
566
+ * What a model line's image input costs at most, as the line's own documented
567
+ * rule states it (contract set 8.2.0, Kaana ADR 0007). Internal: Oxy reads it
568
+ * from the declared catalogue to bound the input of a request carrying images;
569
+ * `GET /v1/models` does not serve it.
570
+ *
571
+ * `maxInputTokensPerImage` is the most input tokens the line's provider bills
572
+ * for ONE image the contract admits (`checkImageInput`), at any `detail` the
573
+ * contract offers, from the publisher's documented resize and tokenization
574
+ * rule. Images are billed as input tokens at the line's `input_tokens` price;
575
+ * this figure only sizes a hold and qualifies a route, the settlement is the
576
+ * provider's own count.
577
+ *
578
+ * ABSENT means undeclared, even on a model whose `inputModalities` lists
579
+ * `image`: nobody has stated what one image costs on it, so no request carrying
580
+ * an image is authorized on it.
581
+ */
582
+ export declare const declaredImageInputSchema: z.ZodObject<{
583
+ maxInputTokensPerImage: z.ZodNumber;
584
+ }, z.core.$strict>;
565
585
  /**
566
586
  * One model as Kaana DECLARES it in `configs/public-catalogue.json` (Kaana ADR
567
587
  * 0002, amendment 2026-10-15): the public `modelSchema` without the three
@@ -791,6 +811,9 @@ export declare const declaredModelSchema: z.ZodObject<{
791
811
  platform_internal: "platform_internal";
792
812
  public: "public";
793
813
  }>;
814
+ imageInput: z.ZodOptional<z.ZodObject<{
815
+ maxInputTokensPerImage: z.ZodNumber;
816
+ }, z.core.$strict>>;
794
817
  }, z.core.$strict>;
795
818
  /**
796
819
  * The declared catalogue: identity, publisher, licence, provenance, data
@@ -1019,9 +1042,13 @@ export declare const declaredCatalogueSchema: z.ZodObject<{
1019
1042
  platform_internal: "platform_internal";
1020
1043
  public: "public";
1021
1044
  }>;
1045
+ imageInput: z.ZodOptional<z.ZodObject<{
1046
+ maxInputTokensPerImage: z.ZodNumber;
1047
+ }, z.core.$strict>>;
1022
1048
  }, z.core.$strict>>;
1023
1049
  }, z.core.$strict>;
1024
1050
  export type DeclaredModel = z.infer<typeof declaredModelSchema>;
1051
+ export type DeclaredImageInput = z.infer<typeof declaredImageInputSchema>;
1025
1052
  export type DeclaredCatalogue = z.infer<typeof declaredCatalogueSchema>;
1026
1053
  export type AvailabilityScope = z.infer<typeof availabilityScopeSchema>;
1027
1054
  export type CommercialPermission = z.infer<typeof commercialPermissionSchema>;
@@ -354,6 +354,28 @@ export const modelCatalogueEntrySchema = z.object({
354
354
  */
355
355
  powerClass: modelPowerClassSchema.optional(),
356
356
  });
357
+ /**
358
+ * What a model line's image input costs at most, as the line's own documented
359
+ * rule states it (contract set 8.2.0, Kaana ADR 0007). Internal: Oxy reads it
360
+ * from the declared catalogue to bound the input of a request carrying images;
361
+ * `GET /v1/models` does not serve it.
362
+ *
363
+ * `maxInputTokensPerImage` is the most input tokens the line's provider bills
364
+ * for ONE image the contract admits (`checkImageInput`), at any `detail` the
365
+ * contract offers, from the publisher's documented resize and tokenization
366
+ * rule. Images are billed as input tokens at the line's `input_tokens` price;
367
+ * this figure only sizes a hold and qualifies a route, the settlement is the
368
+ * provider's own count.
369
+ *
370
+ * ABSENT means undeclared, even on a model whose `inputModalities` lists
371
+ * `image`: nobody has stated what one image costs on it, so no request carrying
372
+ * an image is authorized on it.
373
+ */
374
+ export const declaredImageInputSchema = z
375
+ .object({
376
+ maxInputTokensPerImage: z.number().int().positive().safe(),
377
+ })
378
+ .strict();
357
379
  /**
358
380
  * One model as Kaana DECLARES it in `configs/public-catalogue.json` (Kaana ADR
359
381
  * 0002, amendment 2026-10-15): the public `modelSchema` without the three
@@ -368,8 +390,20 @@ export const modelCatalogueEntrySchema = z.object({
368
390
  */
369
391
  export const declaredModelSchema = modelSchema
370
392
  .omit({ schemaVersion: true, currentRevision: true, availableRevisions: true })
371
- .extend({ audience: z.enum(['public', 'platform_internal']) })
372
- .strict();
393
+ .extend({
394
+ audience: z.enum(['public', 'platform_internal']),
395
+ imageInput: declaredImageInputSchema.optional(),
396
+ })
397
+ .strict()
398
+ .superRefine((model, ctx) => {
399
+ if (model.imageInput !== undefined && !model.capabilities.inputModalities.includes('image')) {
400
+ ctx.addIssue({
401
+ code: 'custom',
402
+ path: ['imageInput'],
403
+ message: 'a model that bounds its image input takes image input',
404
+ });
405
+ }
406
+ });
373
407
  /**
374
408
  * The declared catalogue: identity, publisher, licence, provenance, data
375
409
  * policy, capabilities, voices and the LIST PRICE per unit of every model
@@ -74,6 +74,7 @@ export declare const grantRequestFactsSchema: z.ZodObject<{
74
74
  audioInputMilliseconds: z.ZodOptional<z.ZodNumber>;
75
75
  embeddingInputs: z.ZodOptional<z.ZodNumber>;
76
76
  images: z.ZodOptional<z.ZodNumber>;
77
+ inputImages: z.ZodOptional<z.ZodNumber>;
77
78
  declaredTools: z.ZodNumber;
78
79
  stream: z.ZodBoolean;
79
80
  realtimeSessionKind: z.ZodOptional<z.ZodEnum<{
@@ -125,6 +126,7 @@ export declare const inferenceAuthorizationRequestSchema: z.ZodObject<{
125
126
  audioInputMilliseconds: z.ZodOptional<z.ZodNumber>;
126
127
  embeddingInputs: z.ZodOptional<z.ZodNumber>;
127
128
  images: z.ZodOptional<z.ZodNumber>;
129
+ inputImages: z.ZodOptional<z.ZodNumber>;
128
130
  declaredTools: z.ZodNumber;
129
131
  stream: z.ZodBoolean;
130
132
  realtimeSessionKind: z.ZodOptional<z.ZodEnum<{
@@ -204,6 +206,7 @@ export declare const authorizationRequestSchema: z.ZodDiscriminatedUnion<[z.ZodO
204
206
  audioInputMilliseconds: z.ZodOptional<z.ZodNumber>;
205
207
  embeddingInputs: z.ZodOptional<z.ZodNumber>;
206
208
  images: z.ZodOptional<z.ZodNumber>;
209
+ inputImages: z.ZodOptional<z.ZodNumber>;
207
210
  declaredTools: z.ZodNumber;
208
211
  stream: z.ZodBoolean;
209
212
  realtimeSessionKind: z.ZodOptional<z.ZodEnum<{
@@ -22,7 +22,7 @@ import { maxUtf16Length, minUtf16Length } from '../stringBounds.js';
22
22
  import { powerLevelSchema, realtimeSessionKindSchema } from '../public/catalogue.js';
23
23
  import { generationRecordSchema } from '../public/generations.js';
24
24
  import { delegatedUserIdSchema, generationIdSchema, idempotencyKeySchema, inferenceEnvironmentSchema, inferenceRegionSchema, inferenceTimestampSchema, modelIdSchema, modelRevisionLabelSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, } from '../public/identifiers.js';
25
- import { requestLabelsSchema } from '../public/messages.js';
25
+ import { MAX_INPUT_IMAGES, requestLabelsSchema } from '../public/messages.js';
26
26
  import { moneySchema } from '../public/money.js';
27
27
  import { routingPolicyReferenceSchema } from '../public/policy.js';
28
28
  import { inferenceScopeSchema } from './attribution.js';
@@ -107,6 +107,13 @@ export const grantRequestFactsSchema = z
107
107
  /** Embeddings: how many inputs. Images: how many images. */
108
108
  embeddingInputs: z.number().int().positive().max(2048).optional(),
109
109
  images: z.number().int().positive().max(10).optional(),
110
+ /**
111
+ * Completions: how many image parts the input carries (contract set 8.2.0,
112
+ * Kaana ADR 0007), present exactly when it carries one. Their bytes are not
113
+ * in `estimatedInputTokens`: Oxy bounds each image by the line's declared
114
+ * `imageInput.maxInputTokensPerImage` and admits only lines that declare it.
115
+ */
116
+ inputImages: z.number().int().positive().max(MAX_INPUT_IMAGES).optional(),
110
117
  /** How many tools the request declares; their definitions stay in Kaana. */
111
118
  declaredTools: z.number().int().nonnegative().max(128),
112
119
  stream: z.boolean(),
@@ -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
  *
@@ -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
  *
@@ -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.1.0";
103
+ export declare const INFERENCE_CONTRACT_VERSION = "8.2.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,14 @@ 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.
190
198
  */
191
199
  /** The public API's one origin and version. */
192
200
  export declare const KAANA_API_BASE_URL = "https://kaana.ai/v1";
@@ -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.1.0';
103
+ export const INFERENCE_CONTRACT_VERSION = '8.2.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,14 @@ 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.
190
198
  */
191
199
  /** The public API's one origin and version. */
192
200
  export const KAANA_API_BASE_URL = 'https://kaana.ai/v1';
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Kaana API",
5
- "version": "8.1.0",
5
+ "version": "8.2.0",
6
6
  "description": "Kaana inference API. Generated from @kaana.ai/contracts; the Zod source is the authority. Accounts, applications, oxy_sk_ keys, limits and billing are managed in Oxy Console: https://console.oxy.so"
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaana.ai/contracts",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "Kaana inference contract: the Zod schemas of the public API at https://kaana.ai/v1 and, under /internal, of the Oxy-Kaana grant, settlement and signed lane.",
5
5
  "type": "module",
6
6
  "sideEffects": false,